From 51724ac4ff22b00c7ce12800ceb4de39ef1bc821 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 19 Dec 2023 23:17:53 -0800 Subject: [PATCH 01/66] Flatten service structure Flattens the service structure so that Solana program services aren't nested. Creates a wallet adapter for using the relay service. --- packages/libs/src/sdk/api/users/UsersApi.ts | 38 ++-- packages/libs/src/sdk/sdk.ts | 20 +- .../libs/src/sdk/services/Solana/Solana.ts | 188 ------------------ .../src/sdk/services/Solana/SolanaRelay.ts | 108 ++++++++++ .../Solana/SolanaRelayWalletAdapter.ts | 78 ++++++++ .../libs/src/sdk/services/Solana/constants.ts | 22 -- .../libs/src/sdk/services/Solana/index.ts | 2 +- .../{ => ClaimableTokens}/ClaimableTokens.ts | 62 +++--- .../programs/ClaimableTokens/constants.ts | 11 + .../Solana/programs/ClaimableTokens/types.ts | 80 ++++++++ .../services/Solana/programs/SolanaProgram.ts | 100 ++++++++++ .../src/sdk/services/Solana/programs/types.ts | 97 ++++----- .../libs/src/sdk/services/Solana/types.ts | 73 +------ packages/libs/src/sdk/types.ts | 17 +- 14 files changed, 493 insertions(+), 403 deletions(-) delete mode 100644 packages/libs/src/sdk/services/Solana/Solana.ts create mode 100644 packages/libs/src/sdk/services/Solana/SolanaRelay.ts create mode 100644 packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts delete mode 100644 packages/libs/src/sdk/services/Solana/constants.ts rename packages/libs/src/sdk/services/Solana/programs/{ => ClaimableTokens}/ClaimableTokens.ts (78%) create mode 100644 packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/constants.ts create mode 100644 packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts create mode 100644 packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts diff --git a/packages/libs/src/sdk/api/users/UsersApi.ts b/packages/libs/src/sdk/api/users/UsersApi.ts index 15e106d7f54..29f1a65365a 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.ts @@ -3,7 +3,6 @@ import snakecaseKeys from 'snakecase-keys' import type { AuthService, DiscoveryNodeSelectorService, - SolanaService, StorageService } from '../../services' import { @@ -39,6 +38,7 @@ import { SendTipRequest, SendTipSchema } from './types' +import type { ClaimableTokens } from '../../services/Solana/programs/ClaimableTokens/ClaimableTokens' export class UsersApi extends GeneratedUsersApi { constructor( @@ -48,7 +48,7 @@ export class UsersApi extends GeneratedUsersApi { private readonly entityManager: EntityManagerService, private readonly auth: AuthService, private readonly logger: LoggerService, - private readonly solana: SolanaService + private readonly claimableTokens: ClaimableTokens ) { super(configuration) this.logger = logger.createPrefixedLogger('[users-api]') @@ -442,25 +442,23 @@ export class UsersApi extends GeneratedUsersApi { throw new Error('Invalid recipient: No user bank found.') } - const secp = - await this.solana.ClaimableTokens.createTransferSecpInstruction({ - ethWallet, - destination, - amount, - mint: 'wAUDIO', - auth: this.auth - }) - const transfer = - await this.solana.ClaimableTokens.createTransferInstruction({ - ethWallet, - destination, - mint: 'wAUDIO' - }) - - const transaction = await this.solana.buildTransaction({ + const secp = await this.claimableTokens.createTransferSecpInstruction({ + ethWallet, + destination, + amount, + mint: 'wAUDIO', + auth: this.auth + }) + const transfer = await this.claimableTokens.createTransferInstruction({ + ethWallet, + destination, + mint: 'wAUDIO' + }) + + const transaction = await this.claimableTokens.buildTransaction({ instructions: [secp, transfer] }) - return await this.solana.relay({ transaction }) + return await this.claimableTokens.sendTransaction(transaction) } /** @@ -473,7 +471,7 @@ export class UsersApi extends GeneratedUsersApi { if (!ethWallet) { return { ethWallet: null, userBank: null } } - const { userBank } = await this.solana.ClaimableTokens.getOrCreateUserBank({ + const { userBank } = await this.claimableTokens.getOrCreateUserBank({ ethWallet, mint: 'wAUDIO' }) diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index ee9a290b4d1..bc888c7ed2b 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -34,7 +34,10 @@ import { defaultEntityManagerConfig } from './services/EntityManager/constants' import { Logger } from './services/Logger' import { StorageNodeSelector } from './services/StorageNodeSelector' import { SdkConfig, SdkConfigSchema, ServicesContainer } from './types' -import { Solana } from './services/Solana/Solana' +import { SolanaRelay } from './services/Solana/SolanaRelay' +import { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' +import { SolanaRelayWalletAdapter } from './services/Solana/SolanaRelayWalletAdapter' +import { defaultClaimableTokensConfig } from './services/Solana/programs/ClaimableTokens/constants' /** * The Audius SDK @@ -102,12 +105,19 @@ const initializeServices = (config: SdkConfig) => { const defaultStorage = new Storage({ storageNodeSelector, logger }) - const defaultSolana = new Solana({ + const defaultSolanaRelay = new SolanaRelay({ middleware: [ config.services?.discoveryNodeSelector?.createMiddleware() ?? defaultDiscoveryNodeSelector.createMiddleware() ] }) + const defaultSolanaWalletAdapter = new SolanaRelayWalletAdapter( + config.services?.solanaRelay ?? defaultSolanaRelay + ) + const claimableTokensProgram = new ClaimableTokens( + defaultClaimableTokensConfig, + config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter + ) const defaultServices: ServicesContainer = { storageNodeSelector, @@ -115,7 +125,9 @@ const initializeServices = (config: SdkConfig) => { entityManager: defaultEntityManager, storage: defaultStorage, auth: defaultAuthService, - solana: defaultSolana, + claimableTokensProgram, + solanaWalletAdapter: defaultSolanaWalletAdapter, + solanaRelay: defaultSolanaRelay, logger } return { ...defaultServices, ...config.services } @@ -152,7 +164,7 @@ const initializeApis = ({ services.entityManager, services.auth, services.logger, - services.solana + services.claimableTokensProgram ) const albums = new AlbumsApi( generatedApiClientConfig, diff --git a/packages/libs/src/sdk/services/Solana/Solana.ts b/packages/libs/src/sdk/services/Solana/Solana.ts deleted file mode 100644 index d6e337d9679..00000000000 --- a/packages/libs/src/sdk/services/Solana/Solana.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { - Connection, - PublicKey, - TransactionMessage, - VersionedTransaction -} from '@solana/web3.js' -import { BaseAPI } from '../../api/generated/default' -import * as runtime from '../../api/generated/default/runtime' -import { parseParams } from '../../utils/parseParams' -import { - type RelayRequestBody, - type SolanaConfig, - type SolanaConfigInternal, - RelayRequest, - RelaySchema, - BuildTransactionRequest, - BuildTransactionSchema -} from './types' -import { mergeConfigWithDefaults } from '../../utils/mergeConfigs' -import { defaultSolanaConfig } from './constants' -import fetch from 'cross-fetch' -import { ClaimableTokens } from './programs/ClaimableTokens' - -const isPublicKeyArray = (arr: any[]): arr is PublicKey[] => - arr.every((a) => a instanceof PublicKey) - -export class Solana extends BaseAPI { - /** - * Nested service for interacting with the ClaimableTokensProgram. - */ - public readonly ClaimableTokens: ClaimableTokens - - /** - * Connection to interact with the Solana RPC. - */ - private readonly connection: Connection - - /** - * Configuration passed in by consumer (with defaults). - */ - private readonly config: SolanaConfigInternal - - /** - * Public key of the currently selected transaction fee payer - * from the selected Discovery Node. - */ - private feePayer: PublicKey | null = null - - constructor(config?: SolanaConfig) { - super( - new runtime.Configuration({ - fetchApi: fetch, - basePath: '/solana', - headers: { 'Content-Type': 'application/json' }, - middleware: config?.middleware - }) - ) - this.config = mergeConfigWithDefaults(config, defaultSolanaConfig) - this.connection = new Connection( - this.config.rpcEndpoint, - this.config.rpcConfig - ) - this.ClaimableTokens = new ClaimableTokens( - { - programId: this.config.programIds.claimableTokens, - connection: this.connection, - mints: this.config.mints - }, - this - ) - } - - /** - * Gets a random fee payer public key from the selected discovery node's - * Solana relay plugin. - * - * Used when relay transactions don't specify a fee payer override. - */ - async getFeePayer( - initOverrides?: RequestInit | runtime.InitOverrideFunction - ) { - if (this.feePayer !== null) { - return this.feePayer - } - const headerParameters: runtime.HTTPHeaders = {} - const response = await this.request( - { - path: '/feePayer', - method: 'GET', - headers: headerParameters - }, - initOverrides - ) - const { feePayer } = await new runtime.JSONApiResponse( - response, - (json) => ({ - feePayer: !runtime.exists(json, 'feePayer') - ? undefined - : new PublicKey(json['feePayer'] as string) - }) - ).value() - if (!feePayer) { - throw new Error('Failed to get fee payer!') - } - this.feePayer = feePayer - return this.feePayer - } - - /** - * Relays a transaction to the selected discovery node's Solana relay plugin. - */ - async relay( - params: RelayRequest, - initOverrides?: RequestInit | runtime.InitOverrideFunction - ) { - const { transaction, confirmationOptions, sendOptions } = await parseParams( - 'relay', - RelaySchema - )(params) - - const headerParameters: runtime.HTTPHeaders = {} - const body: RelayRequestBody = { - transaction: Buffer.from(transaction.serialize()).toString('base64'), - confirmationOptions, - sendOptions - } - - const response = await this.request( - { - path: '/relay', - method: 'POST', - headers: headerParameters, - body: body - }, - initOverrides - ) - - return await new runtime.JSONApiResponse(response, (json) => ({ - signature: !runtime.exists(json, 'signature') - ? undefined - : (json['signature'] as string) - })).value() - } - - /** - * Convenience helper to construct v0 transactions. - * - * Handles fetching a recent blockhash, getting lookup table accounts, - * and assigning a fee payer. - */ - async buildTransaction(params: BuildTransactionRequest) { - let { - instructions, - feePayer, - recentBlockhash, - addressLookupTables = [] - } = await parseParams('buildTransaction', BuildTransactionSchema)(params) - - if (!recentBlockhash) { - const res = await this.connection.getLatestBlockhash() - recentBlockhash = res.blockhash - } - - let addressLookupTableAccounts = !isPublicKeyArray(addressLookupTables) - ? addressLookupTables - : await this.getLookupTableAccounts(addressLookupTables) - - const message = new TransactionMessage({ - payerKey: feePayer ?? (await this.getFeePayer()), - recentBlockhash, - instructions - }).compileToV0Message(addressLookupTableAccounts) - - return new VersionedTransaction(message) - } - - private async getLookupTableAccounts(lookupTableKeys: PublicKey[]) { - return await Promise.all( - lookupTableKeys.map(async (accountKey) => { - const res = await this.connection.getAddressLookupTable(accountKey) - if (res.value === null) { - throw new Error(`Lookup table not found: ${accountKey.toBase58()}`) - } - return res.value - }) - ) - } -} diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts new file mode 100644 index 00000000000..71f3a11ae64 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts @@ -0,0 +1,108 @@ +import { PublicKey } from '@solana/web3.js' +import { BaseAPI } from '../../api/generated/default' +import * as runtime from '../../api/generated/default/runtime' +import { parseParams } from '../../utils/parseParams' +import { + type RelayRequestBody, + type SolanaConfig, + RelayRequest, + RelaySchema +} from './types' +import fetch from 'cross-fetch' + +/** + * Client for the Solana Relay Plugin on Discovery. + */ +export class SolanaRelay extends BaseAPI { + /** + * Public key of the currently selected transaction fee payer + * from the selected Discovery Node. + */ + private feePayer: PublicKey | null = null + + constructor(config?: SolanaConfig) { + super( + new runtime.Configuration({ + fetchApi: fetch, + basePath: '/solana', + headers: { 'Content-Type': 'application/json' }, + middleware: config?.middleware + }) + ) + } + + /** + * Gets a random fee payer public key from the selected discovery node's + * Solana relay plugin. + * + * Used when relay transactions don't specify a fee payer override. + */ + public async getFeePayer( + initOverrides?: RequestInit | runtime.InitOverrideFunction + ) { + if (this.feePayer !== null) { + return this.feePayer + } + const headerParameters: runtime.HTTPHeaders = {} + const response = await this.request( + { + path: '/feePayer', + method: 'GET', + headers: headerParameters + }, + initOverrides + ) + const { feePayer } = await new runtime.JSONApiResponse( + response, + (json) => ({ + feePayer: !runtime.exists(json, 'feePayer') + ? undefined + : new PublicKey(json['feePayer'] as string) + }) + ).value() + if (!feePayer) { + throw new Error('Failed to get fee payer!') + } + this.feePayer = feePayer + return this.feePayer + } + + /** + * Relays a transaction to the selected discovery node's Solana relay plugin. + */ + public async relay( + params: RelayRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction + ) { + const { transaction, confirmationOptions, sendOptions } = await parseParams( + 'relay', + RelaySchema + )(params) + + const headerParameters: runtime.HTTPHeaders = {} + const body: RelayRequestBody = { + transaction: Buffer.from(transaction.serialize()).toString('base64'), + confirmationOptions, + sendOptions + } + + const response = await this.request( + { + path: '/relay', + method: 'POST', + headers: headerParameters, + body: body + }, + initOverrides + ) + + return await new runtime.JSONApiResponse(response, (json) => { + if (!runtime.exists(json, 'signature')) { + throw new Error('Signature missing') + } + return { + signature: json['signature'] as string + } + }).value() + } +} diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts new file mode 100644 index 00000000000..da4ed806563 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts @@ -0,0 +1,78 @@ +import { + SupportedTransactionVersions, + TransactionOrVersionedTransaction, + WalletName, + WalletReadyState +} from '@solana/wallet-adapter-base' +import type { PublicKey } from '@solana/web3.js' +import type { SolanaRelay } from './SolanaRelay' +import type { SolanaWalletAdapter } from './types' + +/** + * Wallet adapter that uses the Solana Relay Plugin on Discovery Node. + * + * Implementing this interface allows consumers to choose to pay for their own + * transactions rather than using our relay, simply by using their own wallet + * app's existing wallet adapter in place of this class. + * + * @see {@link https://github.com/solana-labs/wallet-standard/blob/master/WALLET.md wallet-standard} + * @see {@link https://github.com/solana-labs/wallet-adapter/blob/master/packages/wallets/phantom/src/adapter.ts Phantom Wallet Adapter} + */ +export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { + public readonly name = + 'AudiusSolanaWallet' as WalletName<'AudiusSolanaWallet'> + public readonly url = '' + public readonly icon = '' + public readonly readyState: WalletReadyState = WalletReadyState.Loadable + public readonly supportedTransactionVersions?: SupportedTransactionVersions + + private _publicKey: PublicKey | null = null + private _connecting = false + private _connected = true + + constructor(private solanaRelay: SolanaRelay) {} + + public get publicKey() { + return this._publicKey + } + + public get connecting() { + return this._connecting + } + + public get connected() { + return this._connected + } + + public async autoConnect() { + await this.connect() + } + + /** + * On connection, grabs the fee payer from the Discovery Node plugin. + */ + public async connect() { + this._connecting = true + this._publicKey = await this.solanaRelay.getFeePayer() + this._connecting = false + this._connected = true + } + + public async disconnect() { + this._connected = false + this._publicKey = null + } + + /** + * Sends a transaction using the relay to Discovery Node. + * @param transaction the transaction to send. + */ + public async sendTransaction( + transaction: TransactionOrVersionedTransaction< + this['supportedTransactionVersions'] + > + ) { + const { signature } = await this.solanaRelay.relay({ transaction }) + return signature + } +} diff --git a/packages/libs/src/sdk/services/Solana/constants.ts b/packages/libs/src/sdk/services/Solana/constants.ts deleted file mode 100644 index 320e5900215..00000000000 --- a/packages/libs/src/sdk/services/Solana/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import type { SolanaConfigInternal } from './types' - -export const defaultSolanaConfig: SolanaConfigInternal = { - rpcEndpoint: 'https://api.mainnet-beta.solana.com', - mints: { - wAUDIO: new PublicKey('9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM'), - USDC: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v') - }, - programIds: { - claimableTokens: new PublicKey( - 'Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ' - ), - rewardManager: new PublicKey( - 'DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei' - ), - paymentRouter: new PublicKey('paytYpX3LPN98TAeen6bFFeraGSuWnomZmCXjAsoqPa'), - trackListenCount: new PublicKey( - '7K3UpbZViPnQDLn2DAM853B9J5GBxd1L1rLHy4KqSmWG' - ) - } -} diff --git a/packages/libs/src/sdk/services/Solana/index.ts b/packages/libs/src/sdk/services/Solana/index.ts index dc4887da72c..502ec875b4f 100644 --- a/packages/libs/src/sdk/services/Solana/index.ts +++ b/packages/libs/src/sdk/services/Solana/index.ts @@ -1,2 +1,2 @@ -export * from './Solana' +export * from './SolanaRelay' export * from './types' diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/ClaimableTokens.ts similarity index 78% rename from packages/libs/src/sdk/services/Solana/programs/ClaimableTokens.ts rename to packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/ClaimableTokens.ts index 6d24d435db3..4b3be2616b6 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/ClaimableTokens.ts @@ -4,11 +4,10 @@ import { TransactionMessage, VersionedTransaction, Secp256k1Program, - PublicKey, - Connection + PublicKey } from '@solana/web3.js' -import { parseParams } from '../../../utils/parseParams' -import type { Mint, SolanaService } from '../types' +import { parseParams } from '../../../../utils/parseParams' +import type { Mint, SolanaWalletAdapter } from '../../types' import { type GetOrCreateUserBankRequest, @@ -18,36 +17,40 @@ import { CreateTransferSchema, type CreateSecpRequest, CreateSecpSchema, - ClaimableTokensConfigInternal + ClaimableTokensConfig } from './types' -import * as runtime from '../../../api/generated/default/runtime' +import { SolanaProgram } from '../SolanaProgram' +import { defaultClaimableTokensConfig } from './constants' +import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' -export class ClaimableTokens { +export class ClaimableTokens extends SolanaProgram { /** The program ID of the ClaimableTokensProgram instance. */ private readonly programId: PublicKey - /** Connection to interact with the Solana RPC. */ - private readonly connection: Connection /** Map from token mint name to public key address. */ private readonly mints: Record /** Map from token mint name to derived user bank authority. */ private readonly authorities: Record constructor( - config: ClaimableTokensConfigInternal, - private solana: SolanaService + config: ClaimableTokensConfig, + solanaWalletAdapter: SolanaWalletAdapter ) { - this.programId = config.programId - this.connection = config.connection - this.mints = config.mints + const configWithDefaults = mergeConfigWithDefaults( + config, + defaultClaimableTokensConfig + ) + super(configWithDefaults, solanaWalletAdapter) + this.programId = configWithDefaults.programId + this.mints = configWithDefaults.mints this.authorities = { wAUDIO: ClaimableTokensProgram.deriveAuthority({ - programId: config.programId, - mint: config.mints.wAUDIO + programId: configWithDefaults.programId, + mint: configWithDefaults.mints.wAUDIO }), USDC: ClaimableTokensProgram.deriveAuthority({ - programId: config.programId, - mint: config.mints.wAUDIO + programId: configWithDefaults.programId, + mint: configWithDefaults.mints.wAUDIO }) } } @@ -61,13 +64,7 @@ export class ClaimableTokens { GetOrCreateUserBankSchema )(params) const { ethWallet, mint, feePayer: feePayerOverride } = args - const feePayer = feePayerOverride ?? (await this.solana.getFeePayer()) - if (!feePayer) { - throw new runtime.RequiredError( - 'feePayer', - 'Required parameter params.feePayer was null or undefined when calling getOrCreateUserBank.' - ) - } + const feePayer = feePayerOverride ?? (await this.getFeePayer()) const userBank = await this.deriveUserBank(args) const userBankAccount = await this.connection.getAccountInfo(userBank) if (!userBankAccount) { @@ -80,23 +77,14 @@ export class ClaimableTokens { userBank, programId: this.programId }) - const { blockhash, lastValidBlockHeight } = - await this.connection.getLatestBlockhash() + const { blockhash } = await this.connection.getLatestBlockhash() const message = new TransactionMessage({ payerKey: feePayer, recentBlockhash: blockhash, instructions: [createUserBankInstruction] }).compileToLegacyMessage() const transaction = new VersionedTransaction(message) - await this.solana.relay({ - transaction, - confirmationOptions: { - strategy: { - blockhash, - lastValidBlockHeight - } - } - }) + await this.wallet.sendTransaction(transaction, this.connection) return { userBank, didExist: false } } return { userBank, didExist: true } @@ -119,7 +107,7 @@ export class ClaimableTokens { 'createTransferInstruction', CreateTransferSchema )(params) - const feePayer = feePayerOverride ?? (await this.solana.getFeePayer()) + const feePayer = feePayerOverride ?? (await this.getFeePayer()) const source = await this.deriveUserBank({ ethWallet, mint }) const nonceKey = ClaimableTokensProgram.deriveNonce({ ethAddress: ethWallet, diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/constants.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/constants.ts new file mode 100644 index 00000000000..03bb2d15653 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/constants.ts @@ -0,0 +1,11 @@ +import { PublicKey } from '@solana/web3.js' +import type { ClaimableTokensConfigInternal } from './types' + +export const defaultClaimableTokensConfig: ClaimableTokensConfigInternal = { + programId: new PublicKey('Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ'), + rpcEndpoint: 'https://api.mainnet-beta.solana.com', + mints: { + wAUDIO: new PublicKey('9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM'), + USDC: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v') + } +} diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts new file mode 100644 index 00000000000..a0750a8110b --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts @@ -0,0 +1,80 @@ +import { z } from 'zod' +import type { AuthService } from '../../../Auth' +import { Mint, MintSchema, PublicKeySchema } from '../../types' +import type { PublicKey } from '@solana/web3.js' +import type { Prettify } from '../../../../utils/prettify' +import type { SolanaProgramConfigInternal } from '../types' + +export type ClaimableTokensConfigInternal = { + /** The program ID of the ClaimableTokensProgram instance. */ + programId: PublicKey + /** Map from token mint name to public key address. */ + mints: Record +} & SolanaProgramConfigInternal + +export type ClaimableTokensConfig = Prettify< + Partial> & { + mints?: Prettify>> + } +> + +export const GetOrCreateUserBankSchema = z + .object({ + /** The user's Ethereum wallet. */ + ethWallet: z.string(), + /** The name of the token mint. */ + mint: MintSchema, + /** + * The public key of the account that will be paying the + * account creation and transaction fees. + */ + feePayer: PublicKeySchema.optional() + }) + .strict() + +export type GetOrCreateUserBankRequest = z.infer< + typeof GetOrCreateUserBankSchema +> + +export const DeriveUserBankSchema = z + .object({ + /** The user's Ethereum wallet. */ + ethWallet: z.string(), + /** The name of the token mint. */ + mint: MintSchema + }) + .strict() + +export type DeriveUserBankRequest = z.infer + +export const CreateTransferSchema = z.object({ + /** The public key of the account that will be paying the transaction fees. */ + feePayer: PublicKeySchema.optional(), + /** The sending user's Ethereum wallet. */ + ethWallet: z.string(), + /** The name of the token mint. */ + mint: MintSchema, + /** The public key of the destination account. */ + destination: PublicKeySchema +}) + +export type CreateTransferRequest = z.infer + +export const CreateSecpSchema = z + .object({ + /** The sending user's Ethereum wallet. */ + ethWallet: z.string(), + /** The public key of the destination account. */ + destination: PublicKeySchema, + /** The amount to send, either in "lamports"/"wei" (bigint) or decimal number. */ + amount: z.union([z.bigint(), z.number()]), + /** The name of the token mint. */ + mint: MintSchema, + /** The index of this instruction within the transaction. */ + instructionIndex: z.number().optional(), + /** The auth service for signing the instruction data. */ + auth: z.custom() + }) + .strict() + +export type CreateSecpRequest = z.infer diff --git a/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts b/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts new file mode 100644 index 00000000000..a3b396efeb4 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts @@ -0,0 +1,100 @@ +import { + Connection, + PublicKey, + TransactionMessage, + VersionedTransaction +} from '@solana/web3.js' +import type { SolanaWalletAdapter } from '../types' +import { + BuildTransactionRequest, + BuildTransactionSchema, + type SolanaProgramConfigInternal +} from './types' +import { parseParams } from '../../../utils/parseParams' + +const isPublicKeyArray = (arr: any[]): arr is PublicKey[] => + arr.every((a) => a instanceof PublicKey) + +/** + * Abstract class for initializing individual program clients. + */ +export class SolanaProgram { + /** The endpoint for the Solana RPC. */ + protected readonly connection: Connection + constructor( + config: SolanaProgramConfigInternal, + protected wallet: SolanaWalletAdapter + ) { + this.connection = new Connection(config.rpcEndpoint, config.rpcConfig) + } + + /** + * Sends a transaction using the connected wallet adapter and the connection. + * @param transaction The transaction to send. + * @param sendOptions The options to send it with. + */ + public async sendTransaction( + transaction: Parameters[0], + sendOptions?: Parameters[2] + ) { + return await this.wallet.sendTransaction( + transaction, + this.connection, + sendOptions + ) + } + + /** + * Convenience helper to construct v0 transactions. + * + * Handles fetching a recent blockhash, getting lookup table accounts, + * and assigning a fee payer. + */ + public async buildTransaction(params: BuildTransactionRequest) { + let { + instructions, + feePayer, + recentBlockhash, + addressLookupTables = [] + } = await parseParams('buildTransaction', BuildTransactionSchema)(params) + + if (!recentBlockhash) { + const res = await this.connection.getLatestBlockhash() + recentBlockhash = res.blockhash + } + + let addressLookupTableAccounts = !isPublicKeyArray(addressLookupTables) + ? addressLookupTables + : await this.getLookupTableAccounts(addressLookupTables) + + const message = new TransactionMessage({ + payerKey: feePayer ?? (await this.getFeePayer()), + recentBlockhash, + instructions + }).compileToV0Message(addressLookupTableAccounts) + + return new VersionedTransaction(message) + } + + /** + * Gets the fee payer from the connected wallet. + */ + protected async getFeePayer() { + if (!this.wallet.connected) { + await this.wallet.connect() + } + return this.wallet.publicKey! + } + + private async getLookupTableAccounts(lookupTableKeys: PublicKey[]) { + return await Promise.all( + lookupTableKeys.map(async (accountKey) => { + const res = await this.connection.getAddressLookupTable(accountKey) + if (res.value === null) { + throw new Error(`Lookup table not found: ${accountKey.toBase58()}`) + } + return res.value + }) + ) + } +} diff --git a/packages/libs/src/sdk/services/Solana/programs/types.ts b/packages/libs/src/sdk/services/Solana/programs/types.ts index 40646d46f0a..db070248a96 100644 --- a/packages/libs/src/sdk/services/Solana/programs/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/types.ts @@ -1,74 +1,45 @@ +import { + AddressLookupTableAccount, + TransactionInstruction, + type ConnectionConfig +} from '@solana/web3.js' import { z } from 'zod' -import type { AuthService } from '../../Auth' -import { Mint, MintSchema, PublicKeySchema } from '../types' -import type { Connection, PublicKey } from '@solana/web3.js' +import { PublicKeySchema } from '../types' -export type ClaimableTokensConfigInternal = { - /** The program ID of the ClaimableTokensProgram instance. */ - programId: PublicKey +export type SolanaProgramConfigInternal = { /** Connection to interact with the Solana RPC */ - connection: Connection - /** Map from token mint name to public key address. */ - mints: Record + rpcEndpoint: string + /** Configuration to use for the RPC connection. */ + rpcConfig?: ConnectionConfig } -export const GetOrCreateUserBankSchema = z +export const BuildTransactionSchema = z .object({ - /** The user's Ethereum wallet. */ - ethWallet: z.string(), - /** The name of the token mint. */ - mint: MintSchema, + instructions: z + .array( + z.custom( + (instr) => instr instanceof TransactionInstruction + ) + ) + .min(1), + recentBlockhash: z.string().optional(), + feePayer: PublicKeySchema.optional(), /** - * The public key of the account that will be paying the - * account creation and transaction fees. + * Either the public keys or actual account data for related address lookup tables. */ - feePayer: PublicKeySchema.optional() + addressLookupTables: z + .union([ + z.array(PublicKeySchema).default([]), + z + .array( + z.custom( + (arg) => arg instanceof AddressLookupTableAccount + ) + ) + .default([]) + ]) + .optional() }) .strict() -export type GetOrCreateUserBankRequest = z.infer< - typeof GetOrCreateUserBankSchema -> - -export const DeriveUserBankSchema = z - .object({ - /** The user's Ethereum wallet. */ - ethWallet: z.string(), - /** The name of the token mint. */ - mint: MintSchema - }) - .strict() - -export type DeriveUserBankRequest = z.infer - -export const CreateTransferSchema = z.object({ - /** The public key of the account that will be paying the transaction fees. */ - feePayer: PublicKeySchema.optional(), - /** The sending user's Ethereum wallet. */ - ethWallet: z.string(), - /** The name of the token mint. */ - mint: MintSchema, - /** The public key of the destination account. */ - destination: PublicKeySchema -}) - -export type CreateTransferRequest = z.infer - -export const CreateSecpSchema = z - .object({ - /** The sending user's Ethereum wallet. */ - ethWallet: z.string(), - /** The public key of the destination account. */ - destination: PublicKeySchema, - /** The amount to send, either in "lamports"/"wei" (bigint) or decimal number. */ - amount: z.union([z.bigint(), z.number()]), - /** The name of the token mint. */ - mint: MintSchema, - /** The index of this instruction within the transaction. */ - instructionIndex: z.number().optional(), - /** The auth service for signing the instruction data. */ - auth: z.custom() - }) - .strict() - -export type CreateSecpRequest = z.infer +export type BuildTransactionRequest = z.infer diff --git a/packages/libs/src/sdk/services/Solana/types.ts b/packages/libs/src/sdk/services/Solana/types.ts index 9cd9b157b42..6c073892c0b 100644 --- a/packages/libs/src/sdk/services/Solana/types.ts +++ b/packages/libs/src/sdk/services/Solana/types.ts @@ -1,53 +1,27 @@ import { PublicKey, - TransactionInstruction, VersionedTransaction, - ConnectionConfig, SendOptions, - AddressLookupTableAccount + Transaction } from '@solana/web3.js' +import type { WalletAdapterProps } from '@solana/wallet-adapter-base' import type * as runtime from '../../api/generated/default/runtime' import { z } from 'zod' import type { Prettify } from '../../utils/prettify' -import type { Solana } from './Solana' +import type { SolanaRelay } from './SolanaRelay' export type SolanaConfigInternal = { /** * Middleware for HTTP requests to the Solana relay service. */ middleware?: runtime.Middleware[] - /** - * Map from token mint name to public key address. - */ - mints: Record - /** - * Map from program name to program ID public key address. - */ - programIds: Record - /** - * The endpoint to use for the RPC. - */ - rpcEndpoint: string - /** - * Configuration to use for the RPC connection. - */ - rpcConfig?: ConnectionConfig } -export type SolanaConfig = Prettify< - Partial> & { - mints?: Prettify>> - programIds?: Prettify>> - } -> +export type SolanaWalletAdapter = WalletAdapterProps -export type SolanaService = Solana +export type SolanaConfig = Partial -export type Program = - | 'claimableTokens' - | 'rewardManager' - | 'paymentRouter' - | 'trackListenCount' +export type SolanaRelayService = SolanaRelay export const MintSchema = z.enum(['wAUDIO', 'USDC']).default('wAUDIO') @@ -64,8 +38,8 @@ export const PublicKeySchema = z.custom((data) => { export const RelaySchema = z .object({ - transaction: z.custom( - (tx) => tx instanceof VersionedTransaction + transaction: z.custom( + (tx) => tx instanceof VersionedTransaction || tx instanceof Transaction ), /** * Confirmation options used when sending the transaction on the server. @@ -128,34 +102,3 @@ export type RelayRequestBody = Prettify< transaction: string } > - -export const BuildTransactionSchema = z - .object({ - instructions: z - .array( - z.custom( - (instr) => instr instanceof TransactionInstruction - ) - ) - .min(1), - recentBlockhash: z.string().optional(), - feePayer: PublicKeySchema.optional(), - /** - * Either the public keys or actual account data for related address lookup tables. - */ - addressLookupTables: z - .union([ - z.array(PublicKeySchema).default([]), - z - .array( - z.custom( - (arg) => arg instanceof AddressLookupTableAccount - ) - ) - .default([]) - ]) - .optional() - }) - .strict() - -export type BuildTransactionRequest = z.infer diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index ce4a5acbc68..91fa5aaf567 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -6,7 +6,8 @@ import type { EntityManagerService } from './services/EntityManager' import type { LoggerService } from './services/Logger' import type { StorageService } from './services/Storage' import type { StorageNodeSelectorService } from './services/StorageNodeSelector' -import type { SolanaService } from './services/Solana' +import type { SolanaRelayService, SolanaWalletAdapter } from './services/Solana' +import type { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' export type ServicesContainer = { /** @@ -40,9 +41,19 @@ export type ServicesContainer = { logger: LoggerService /** - * Service used to interact with Solana programs + * Service used to interact with the Solana relay */ - solana: SolanaService + solanaRelay: SolanaRelayService + + /** + * Service used to interact with the Solana blockchain + */ + solanaWalletAdapter: SolanaWalletAdapter + + /** + * Claimable Tokens Program client for Solana + */ + claimableTokensProgram: ClaimableTokens } const DevAppSchema = z.object({ From abf1306d04299df8b0e7c7fe511fec9eaec15c51 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 22:29:24 -0800 Subject: [PATCH 02/66] allow solana-test-validator websockets through nginx --- dev-tools/compose/nginx_ingress.conf | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dev-tools/compose/nginx_ingress.conf b/dev-tools/compose/nginx_ingress.conf index ac22279426b..0600a804d7f 100644 --- a/dev-tools/compose/nginx_ingress.conf +++ b/dev-tools/compose/nginx_ingress.conf @@ -252,6 +252,10 @@ server { # # ETH, POA, SOL # +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} server { listen 80; server_name audius-protocol-eth-ganache-1; @@ -281,6 +285,21 @@ server { server_name audius-protocol-solana-test-validator-1; location / { + try_files /nonexistent @$http_upgrade; + } + + location @websocket { + resolver 127.0.0.11 valid=30s; + set $upstream audius-protocol-solana-test-validator-1:8900; + proxy_http_version 1.1; + proxy_pass http://$upstream; + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + location @ { resolver 127.0.0.11 valid=30s; set $upstream audius-protocol-solana-test-validator-1:8899; proxy_pass http://$upstream; From 0df28e7192c201f4b030af71204511c274cddd50 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 22:29:59 -0800 Subject: [PATCH 03/66] increase max connections on local dev db --- dev-tools/compose/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-tools/compose/docker-compose.yml b/dev-tools/compose/docker-compose.yml index 8b400b09588..2a1574c4a37 100644 --- a/dev-tools/compose/docker-compose.yml +++ b/dev-tools/compose/docker-compose.yml @@ -41,7 +41,7 @@ services: db: image: postgres:11.4 shm_size: 2g - command: postgres -c shared_buffers=2GB + command: postgres -c shared_buffers=2GB -c max_connections=300 restart: unless-stopped ports: - 5432:5432 From ff5e7997195b11ee992501623df2bfe334aad3f5 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:15:42 -0800 Subject: [PATCH 04/66] include owner wallets on discovery healthcheck services list --- .../src/tasks/cache_current_nodes.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/discovery-provider/src/tasks/cache_current_nodes.py b/packages/discovery-provider/src/tasks/cache_current_nodes.py index 9b4ec24749e..30f67af5003 100644 --- a/packages/discovery-provider/src/tasks/cache_current_nodes.py +++ b/packages/discovery-provider/src/tasks/cache_current_nodes.py @@ -39,7 +39,16 @@ def cache_current_nodes_task(self): if current_node is not None: discovery_nodes.append(current_node) - set_json_cached_key(redis, ALL_DISCOVERY_NODES_CACHE_KEY, discovery_nodes) + set_json_cached_key( + redis, + ALL_DISCOVERY_NODES_CACHE_KEY, + [ + {"endpoint": endpoint, "delegateOwnerWallet": delegateOwnerWallet} + for endpoint, delegateOwnerWallet in zip( + discovery_nodes, discovery_nodes_wallets + ) + ], + ) set_json_cached_key( redis, ALL_DISCOVERY_NODES_WALLETS_CACHE_KEY, discovery_nodes_wallets ) From 6829698bb9e54a33e6725f0d50fb53cf84ef66a9 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:16:53 -0800 Subject: [PATCH 05/66] log serialized transactions in relay so they can be inspected --- .../pedalboard/apps/solana-relay/src/routes/relay/relay.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/relay.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/relay.ts index a87138f1e80..c116d3c6bdb 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/relay.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/relay.ts @@ -105,6 +105,7 @@ export const relay = async ( }) logger.info('Sending transaction...') const serializedTx = transaction.serialize() + logger.info(`Serialized: ${Buffer.from(serializedTx).toString('base64')}`) const signature = await connection.sendRawTransaction( serializedTx, From 8cb52870299c0c348c4afc867c7669f063cad994 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:18:24 -0800 Subject: [PATCH 06/66] force attestations to use lowercase addresses --- .../src/queries/get_attestation.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/discovery-provider/src/queries/get_attestation.py b/packages/discovery-provider/src/queries/get_attestation.py index 9b372f3c8f9..d96a03006cb 100644 --- a/packages/discovery-provider/src/queries/get_attestation.py +++ b/packages/discovery-provider/src/queries/get_attestation.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime from typing import Tuple @@ -103,7 +104,7 @@ def is_valid_oracle(address: str) -> bool: oracle_addresses = oracle_addresses.decode().split(",") else: oracle_addresses = get_oracle_addresses_from_chain(redis) - return address in oracle_addresses + return address.lower() in [a.lower() for a in oracle_addresses] def sign_attestation(attestation_bytes: bytes, private_key: str): @@ -193,8 +194,8 @@ def get_attestation( attestation = Attestation( amount=str(user_challenge.amount), - oracle_address=oracle_address, - user_address=user_address, + oracle_address=oracle_address.lower(), + user_address=user_address.lower(), challenge_id=challenge.id, challenge_specifier=user_challenge.specifier, ) @@ -203,7 +204,10 @@ def get_attestation( signed_attestation: str = sign_attestation( attestation_bytes, shared_config["delegate"]["private_key"] ) - return (shared_config["delegate"]["owner_wallet"], signed_attestation) + return ( + shared_config["delegate"]["owner_wallet"], + signed_attestation, + ) ADD_SENDER_MESSAGE_PREFIX = "add" From 6ecee480f56544acc3da57d0a618ef6c680eda41 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:20:43 -0800 Subject: [PATCH 07/66] add challenge attestations api to full namespace and gen sdk --- packages/discovery-provider/src/api/v1/api.py | 2 + .../src/api/v1/challenges.py | 29 ++++-- .../generated/full/.openapi-generator/FILES | 3 + .../api/generated/full/apis/ChallengesApi.ts | 93 +++++++++++++++++++ .../src/sdk/api/generated/full/apis/index.ts | 1 + .../api/generated/full/models/Attestation.ts | 76 +++++++++++++++ .../full/models/AttestationReponse.ts | 73 +++++++++++++++ .../sdk/api/generated/full/models/index.ts | 2 + 8 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 packages/libs/src/sdk/api/generated/full/apis/ChallengesApi.ts create mode 100644 packages/libs/src/sdk/api/generated/full/models/Attestation.ts create mode 100644 packages/libs/src/sdk/api/generated/full/models/AttestationReponse.ts diff --git a/packages/discovery-provider/src/api/v1/api.py b/packages/discovery-provider/src/api/v1/api.py index d298a52cbf6..62489549e15 100644 --- a/packages/discovery-provider/src/api/v1/api.py +++ b/packages/discovery-provider/src/api/v1/api.py @@ -2,6 +2,7 @@ from flask.helpers import url_for from flask_restx import Api +from src.api.v1.challenges import full_ns as full_challenges_ns from src.api.v1.challenges import ns as challenges_ns from src.api.v1.cid_data import full_ns as cid_data_full_ns from src.api.v1.dashboard_wallet_users import ns as dashboard_wallet_users_ns @@ -60,3 +61,4 @@ def specs_url(self): api_v1_full.add_namespace(full_transactions_ns) api_v1_full.add_namespace(cid_data_full_ns) api_v1_full.add_namespace(notifications_full_ns) +api_v1_full.add_namespace(full_challenges_ns) diff --git a/packages/discovery-provider/src/api/v1/challenges.py b/packages/discovery-provider/src/api/v1/challenges.py index 1a3cc42c4db..eaf7d6ca79c 100644 --- a/packages/discovery-provider/src/api/v1/challenges.py +++ b/packages/discovery-provider/src/api/v1/challenges.py @@ -36,9 +36,10 @@ logger = logging.getLogger(__name__) ns = Namespace("challenges", description="Challenge related operations") +full_ns = Namespace("challenges", description="Challenge related operations") attestation_response = make_response( - "attestation_reponse", ns, fields.Nested(attestation) + "attestation_reponse", full_ns, fields.Nested(attestation) ) attest_route = "//attest" @@ -61,9 +62,9 @@ ) -@ns.route(attest_route, doc=False) -class Attest(Resource): - @ns.doc( +@full_ns.route(attest_route) +class FullAttest(Resource): + @full_ns.doc( id="Get Challenge Attestation", description="Produces an attestation that a given user has completed a challenge, or errors.", params={ @@ -75,19 +76,19 @@ class Attest(Resource): 500: "Server error", }, ) - @ns.expect(attest_parser) - @ns.marshal_with(attestation_response) + @full_ns.expect(attest_parser) + @full_ns.marshal_with(attestation_response) @cache(ttl_sec=5) def get(self, challenge_id: str): args = attest_parser.parse_args(strict=True) user_id: str = args["user_id"] oracle_address: str = args["oracle"] specifier: str = args["specifier"] - decoded_user_id = decode_with_abort(user_id, ns) + decoded_user_id = decode_with_abort(user_id, full_ns) db = get_db_read_replica() with db.scoped_session() as session: try: - owner_wallet, signature = get_attestation( + owner_wallet, signature, bytes = get_attestation( session, user_id=decoded_user_id, oracle_address=oracle_address, @@ -96,13 +97,23 @@ def get(self, challenge_id: str): ) return success_response( - {"owner_wallet": owner_wallet, "attestation": signature} + { + "owner_wallet": owner_wallet, + "attestation": signature, + "bytes": bytes.hex(), + } ) except AttestationError as e: abort(400, e) return None +@ns.route(attest_route, doc=False) +class Attest(FullAttest): + def get(self, challenge_id: str): + super(self, challenge_id) + + undisbursed_route = "/undisbursed" get_undisbursed_challenges_route_parser = pagination_parser.copy() diff --git a/packages/libs/src/sdk/api/generated/full/.openapi-generator/FILES b/packages/libs/src/sdk/api/generated/full/.openapi-generator/FILES index 34a5b44169d..87154dbf0ce 100644 --- a/packages/libs/src/sdk/api/generated/full/.openapi-generator/FILES +++ b/packages/libs/src/sdk/api/generated/full/.openapi-generator/FILES @@ -1,3 +1,4 @@ +apis/ChallengesApi.ts apis/PlaylistsApi.ts apis/ReactionsApi.ts apis/SearchApi.ts @@ -8,6 +9,8 @@ apis/UsersApi.ts apis/index.ts index.ts models/ActivityFull.ts +models/Attestation.ts +models/AttestationReponse.ts models/CollectionActivityFull.ts models/CollectionLibraryResponseFull.ts models/CoverArt.ts diff --git a/packages/libs/src/sdk/api/generated/full/apis/ChallengesApi.ts b/packages/libs/src/sdk/api/generated/full/apis/ChallengesApi.ts new file mode 100644 index 00000000000..4e2d3d009b2 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/full/apis/ChallengesApi.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +// @ts-nocheck +/* eslint-disable */ +/** + * API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + AttestationReponse, +} from '../models'; +import { + AttestationReponseFromJSON, + AttestationReponseToJSON, +} from '../models'; + +export interface GetChallengeAttestationRequest { + challengeId: string; + oracle: string; + specifier: string; + userId: string; +} + +/** + * + */ +export class ChallengesApi extends runtime.BaseAPI { + + /** + * @hidden + * Produces an attestation that a given user has completed a challenge, or errors. + */ + async getChallengeAttestationRaw(params: GetChallengeAttestationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (params.challengeId === null || params.challengeId === undefined) { + throw new runtime.RequiredError('challengeId','Required parameter params.challengeId was null or undefined when calling getChallengeAttestation.'); + } + + if (params.oracle === null || params.oracle === undefined) { + throw new runtime.RequiredError('oracle','Required parameter params.oracle was null or undefined when calling getChallengeAttestation.'); + } + + if (params.specifier === null || params.specifier === undefined) { + throw new runtime.RequiredError('specifier','Required parameter params.specifier was null or undefined when calling getChallengeAttestation.'); + } + + if (params.userId === null || params.userId === undefined) { + throw new runtime.RequiredError('userId','Required parameter params.userId was null or undefined when calling getChallengeAttestation.'); + } + + const queryParameters: any = {}; + + if (params.oracle !== undefined) { + queryParameters['oracle'] = params.oracle; + } + + if (params.specifier !== undefined) { + queryParameters['specifier'] = params.specifier; + } + + if (params.userId !== undefined) { + queryParameters['user_id'] = params.userId; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/challenges/{challenge_id}/attest`.replace(`{${"challenge_id"}}`, encodeURIComponent(String(params.challengeId))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AttestationReponseFromJSON(jsonValue)); + } + + /** + * Produces an attestation that a given user has completed a challenge, or errors. + */ + async getChallengeAttestation(params: GetChallengeAttestationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getChallengeAttestationRaw(params, initOverrides); + return await response.value(); + } + +} diff --git a/packages/libs/src/sdk/api/generated/full/apis/index.ts b/packages/libs/src/sdk/api/generated/full/apis/index.ts index c9b41fe8c57..98f33ce7964 100644 --- a/packages/libs/src/sdk/api/generated/full/apis/index.ts +++ b/packages/libs/src/sdk/api/generated/full/apis/index.ts @@ -1,5 +1,6 @@ /* tslint:disable */ /* eslint-disable */ +export * from './ChallengesApi'; export * from './PlaylistsApi'; export * from './ReactionsApi'; export * from './SearchApi'; diff --git a/packages/libs/src/sdk/api/generated/full/models/Attestation.ts b/packages/libs/src/sdk/api/generated/full/models/Attestation.ts new file mode 100644 index 00000000000..ccaf86d1120 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/full/models/Attestation.ts @@ -0,0 +1,76 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface Attestation + */ +export interface Attestation { + /** + * + * @type {string} + * @memberof Attestation + */ + ownerWallet: string; + /** + * + * @type {string} + * @memberof Attestation + */ + attestation: string; +} + +/** + * Check if a given object implements the Attestation interface. + */ +export function instanceOfAttestation(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "ownerWallet" in value; + isInstance = isInstance && "attestation" in value; + + return isInstance; +} + +export function AttestationFromJSON(json: any): Attestation { + return AttestationFromJSONTyped(json, false); +} + +export function AttestationFromJSONTyped(json: any, ignoreDiscriminator: boolean): Attestation { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'ownerWallet': json['owner_wallet'], + 'attestation': json['attestation'], + }; +} + +export function AttestationToJSON(value?: Attestation | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'owner_wallet': value.ownerWallet, + 'attestation': value.attestation, + }; +} + diff --git a/packages/libs/src/sdk/api/generated/full/models/AttestationReponse.ts b/packages/libs/src/sdk/api/generated/full/models/AttestationReponse.ts new file mode 100644 index 00000000000..a913b30357c --- /dev/null +++ b/packages/libs/src/sdk/api/generated/full/models/AttestationReponse.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { Attestation } from './Attestation'; +import { + AttestationFromJSON, + AttestationFromJSONTyped, + AttestationToJSON, +} from './Attestation'; + +/** + * + * @export + * @interface AttestationReponse + */ +export interface AttestationReponse { + /** + * + * @type {Attestation} + * @memberof AttestationReponse + */ + data?: Attestation; +} + +/** + * Check if a given object implements the AttestationReponse interface. + */ +export function instanceOfAttestationReponse(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function AttestationReponseFromJSON(json: any): AttestationReponse { + return AttestationReponseFromJSONTyped(json, false); +} + +export function AttestationReponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): AttestationReponse { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'data': !exists(json, 'data') ? undefined : AttestationFromJSON(json['data']), + }; +} + +export function AttestationReponseToJSON(value?: AttestationReponse | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'data': AttestationToJSON(value.data), + }; +} + diff --git a/packages/libs/src/sdk/api/generated/full/models/index.ts b/packages/libs/src/sdk/api/generated/full/models/index.ts index 024355854c0..47721984394 100644 --- a/packages/libs/src/sdk/api/generated/full/models/index.ts +++ b/packages/libs/src/sdk/api/generated/full/models/index.ts @@ -1,6 +1,8 @@ /* tslint:disable */ /* eslint-disable */ export * from './ActivityFull'; +export * from './Attestation'; +export * from './AttestationReponse'; export * from './CollectionActivityFull'; export * from './CollectionLibraryResponseFull'; export * from './CoverArt'; From 6ede5e093e54b21ae88086dd8f87348da3a024dd Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:21:19 -0800 Subject: [PATCH 08/66] remove doesFollowCurrentUser from sdk --- .../src/sdk/api/generated/default/.openapi-generator/VERSION | 2 +- .../libs/src/sdk/api/generated/full/.openapi-generator/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION b/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION index 757e6740040..0f78c31cdc7 100644 --- a/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION +++ b/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION @@ -1 +1 @@ -7.0.0-SNAPSHOT \ No newline at end of file +7.2.0-SNAPSHOT \ No newline at end of file diff --git a/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION b/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION index 757e6740040..0f78c31cdc7 100644 --- a/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION +++ b/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION @@ -1 +1 @@ -7.0.0-SNAPSHOT \ No newline at end of file +7.2.0-SNAPSHOT \ No newline at end of file From 937e0999c5274af482ae091880c3c63c6ef04d8d Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:24:25 -0800 Subject: [PATCH 09/66] refactor audius/spl rewardmanager program, fix BorshString, add vitest --- package-lock.json | 3 + packages/spl/package.json | 5 +- packages/spl/src/index.ts | 2 +- packages/spl/src/layout-utils.ts | 12 +- .../src/reward-manager/AttestationLayout.ts | 70 ++ .../RewardManagerProgram.test.ts | 179 +++++ .../reward-manager/RewardManagerProgram.ts | 610 ++++++++++++++++++ packages/spl/src/reward-manager/constants.ts | 6 - .../spl/src/reward-manager/createSender.ts | 94 --- .../src/reward-manager/createSenderPublic.ts | 112 ---- packages/spl/src/reward-manager/decode.ts | 54 -- .../src/reward-manager/deleteSenderPublic.ts | 40 -- .../reward-manager/evaluateAttestations.ts | 130 ---- packages/spl/src/reward-manager/index.ts | 7 - .../src/reward-manager/submitAttestation.ts | 101 --- packages/spl/src/reward-manager/types.ts | 297 +++++++++ 16 files changed, 1170 insertions(+), 552 deletions(-) create mode 100644 packages/spl/src/reward-manager/AttestationLayout.ts create mode 100644 packages/spl/src/reward-manager/RewardManagerProgram.test.ts create mode 100644 packages/spl/src/reward-manager/RewardManagerProgram.ts delete mode 100644 packages/spl/src/reward-manager/createSender.ts delete mode 100644 packages/spl/src/reward-manager/createSenderPublic.ts delete mode 100644 packages/spl/src/reward-manager/decode.ts delete mode 100644 packages/spl/src/reward-manager/deleteSenderPublic.ts delete mode 100644 packages/spl/src/reward-manager/evaluateAttestations.ts delete mode 100644 packages/spl/src/reward-manager/index.ts delete mode 100644 packages/spl/src/reward-manager/submitAttestation.ts create mode 100644 packages/spl/src/reward-manager/types.ts diff --git a/package-lock.json b/package-lock.json index 0a2ed4ec56f..11c0a35ff5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -148860,6 +148860,9 @@ "@solana/buffer-layout-utils": "0.2.0", "@solana/spl-token": "0.3.8", "@solana/web3.js": "1.78.4" + }, + "devDependencies": { + "vitest": "0.34.6" } }, "packages/sql-ts": { diff --git a/packages/spl/package.json b/packages/spl/package.json index 4625aa336f5..c56a4039be8 100644 --- a/packages/spl/package.json +++ b/packages/spl/package.json @@ -8,7 +8,7 @@ "scripts": { "start": "tsc --build --verbose --watch tsconfig.all.json", "build": "tsc --build --verbose tsconfig.all.json", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest" }, "repository": { "type": "git", @@ -26,5 +26,8 @@ "@solana/buffer-layout-utils": "0.2.0", "@solana/spl-token": "0.3.8", "@solana/web3.js": "1.78.4" + }, + "devDependencies": { + "vitest": "0.34.6" } } diff --git a/packages/spl/src/index.ts b/packages/spl/src/index.ts index 6845153be67..e5dc0f15031 100644 --- a/packages/spl/src/index.ts +++ b/packages/spl/src/index.ts @@ -1,5 +1,5 @@ export { ClaimableTokensProgram } from './claimable-tokens/ClaimableTokensProgram' -export * from './reward-manager' +export { RewardManagerProgram } from './reward-manager/RewardManagerProgram' export { ethAddress } from './layout-utils' export * from './associated-token' export * from './payment-router' diff --git a/packages/spl/src/layout-utils.ts b/packages/spl/src/layout-utils.ts index ee026af0c43..99dae2b9c63 100644 --- a/packages/spl/src/layout-utils.ts +++ b/packages/spl/src/layout-utils.ts @@ -26,7 +26,7 @@ export class EthereumAddress extends Layout { * converts the buffer to hex and prepends '0x'. * @override * */ - decode(b: Uint8Array, offset = 0): string { + decode(b: Uint8Array, offset = 0) { const buffer = this.blob.decode(b, offset) return '0x' + Buffer.from(buffer).toString('hex') } @@ -36,7 +36,7 @@ export class EthereumAddress extends Layout { * proxies the encoding to the underlying Blob. * @override * */ - encode(src: string, b: Uint8Array, offset = 0): number { + encode(src: string, b: Uint8Array, offset = 0) { const strippedEthAddress = src.replace('0x', '') // Need to pad the array to length 20 - otherwise, hex eth keys starting with '0' would // result in truncated arrays, while eth spec is always 20 bytes @@ -51,13 +51,13 @@ export class EthereumAddress extends Layout { * Wrapper that encodes strings the way Borsh does, with the length prepended */ export class BorshString extends Layout { - constructor(maxLength: number, property?: string) { - super(u32().span + maxLength, property) + constructor(private readonly maxLength: number, property?: string) { + super(-1, property) } getSpan(b: Uint8Array, offset = 0): number { if (!b) { - return this.span + return this.maxLength } const length = u32().decode(b, offset) return u32().span + length @@ -71,7 +71,7 @@ export class BorshString extends Layout { encode(src: string, b: Uint8Array, offset: number): number { const srcb = Buffer.from(src, 'utf-8') - if (srcb.length > this.span) { + if (srcb.length > this.maxLength) { throw new RangeError('text exceeds maxLength') } if (offset + srcb.length > b.length) { diff --git a/packages/spl/src/reward-manager/AttestationLayout.ts b/packages/spl/src/reward-manager/AttestationLayout.ts new file mode 100644 index 00000000000..6b348b8c153 --- /dev/null +++ b/packages/spl/src/reward-manager/AttestationLayout.ts @@ -0,0 +1,70 @@ +import { Layout, blob, utf8 } from '@solana/buffer-layout' +import { u64 } from '@solana/buffer-layout-utils' + +import { ethAddress } from '../layout-utils' + +import { Attestation } from './types' + +const delimiter = Buffer.from('_', 'utf-8') +export class AttestationLayout extends Layout { + constructor(property?: string) { + super( + // ethAddress().span + 1 + u64().span + 1 + 32 + 1 + ethAddress().span, + // 20 + 1 + 8 + 1 + 32 + 1 + 20 = 83 + 83, + property + ) + } + + getSpan(b: Uint8Array, offset = 0) { + return this.span + } + + decode(b: Uint8Array, offset = 0): Attestation { + const recipientEthAddress = ethAddress().decode(b, offset) + offset += ethAddress().span + 1 + const amount = u64().decode(b, offset) + offset += u64().span + 1 + const delimiterIndex = b + .slice(offset) + .findIndex((v) => v === delimiter[0] || v === 0) + const disbursementSpan = + delimiterIndex > -1 ? delimiterIndex : b.byteLength - offset + const disbursementIdBlob = blob(disbursementSpan).decode(b, offset) + const disbursementId = Buffer.from(disbursementIdBlob).toString('utf-8') + offset += disbursementSpan + 1 + return { + recipientEthAddress, + amount, + disbursementId, + antiAbuseOracleEthAddress: + offset < b.byteLength ? ethAddress().decode(b, offset) : null + } + } + + encode(src: Attestation, b: Uint8Array, offset = 0) { + let layoutOffset = offset + layoutOffset += ethAddress().encode( + src.recipientEthAddress, + b, + layoutOffset + ) + layoutOffset += blob(1).encode(delimiter, b, layoutOffset) + layoutOffset += u64().encode(src.amount, b, layoutOffset) + layoutOffset += blob(1).encode(delimiter, b, layoutOffset) + layoutOffset += utf8(32).encode(src.disbursementId, b, layoutOffset) + + if (src.antiAbuseOracleEthAddress) { + layoutOffset += blob(1).encode(delimiter, b, layoutOffset) + layoutOffset += ethAddress().encode( + src.antiAbuseOracleEthAddress, + b, + layoutOffset + ) + } + return layoutOffset - offset + } +} + +export const attestationLayout = (property?: string) => + new AttestationLayout(property) diff --git a/packages/spl/src/reward-manager/RewardManagerProgram.test.ts b/packages/spl/src/reward-manager/RewardManagerProgram.test.ts new file mode 100644 index 00000000000..474780d568d --- /dev/null +++ b/packages/spl/src/reward-manager/RewardManagerProgram.test.ts @@ -0,0 +1,179 @@ +import { PublicKey } from '@solana/web3.js' +import { describe, it, expect } from 'vitest' + +import { RewardManagerProgram } from './RewardManagerProgram' + +describe('RewardManagerProgram', () => { + it('encodes the Discovery attestation instruction data correctly', () => { + const args = { + disbursementId: 'track-upload:1', + recipientEthAddress: '0x055FA758c77D68a04990E992aA4dcdeF899F654A', + antiAbuseOracleEthAddress: '0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3', + amount: BigInt(123) + } + const data = RewardManagerProgram.encodeAttestation(args) + expect(data.subarray(0, 20).toString('hex')).toBe( + args.recipientEthAddress.substring(2).toLowerCase() + ) + expect(data.subarray(20, 21).toString('utf-8')).toBe('_') + expect(data.subarray(21, 29).readBigInt64LE()).toBe(args.amount) + expect(data.subarray(29, 30).toString('utf-8')).toBe('_') + expect(data.subarray(30, 44).toString('utf-8')).toBe(args.disbursementId) + expect(data.subarray(44, 45).toString('utf-8')).toBe('_') + expect(data.subarray(45, 65).toString('hex')).toBe( + args.antiAbuseOracleEthAddress.substring(2).toLowerCase() + ) + }) + + it('decodes the Discovery attestation instruction data correctly', () => { + const args = { + disbursementId: 'track-upload:1', + recipientEthAddress: '0x055FA758c77D68a04990E992aA4dcdeF899F654A', + antiAbuseOracleEthAddress: '0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3', + amount: BigInt(123) + } + const data = Buffer.from([ + 5, 95, 167, 88, 199, 125, 104, 160, 73, 144, 233, 146, 170, 77, 205, 239, + 137, 159, 101, 74, 95, 123, 0, 0, 0, 0, 0, 0, 0, 95, 116, 114, 97, 99, + 107, 45, 117, 112, 108, 111, 97, 100, 58, 49, 95, 240, 213, 188, 24, 66, + 31, 160, 77, 10, 42, 46, 245, 64, 186, 90, 159, 4, 1, 75, 227 + ]) + const attestation = RewardManagerProgram.decodeAttestation(data) + expect(attestation.recipientEthAddress).toBe( + args.recipientEthAddress.toLowerCase() + ) + expect(attestation.amount).toBe(args.amount) + expect(attestation.disbursementId).toBe(args.disbursementId) + expect(attestation.antiAbuseOracleEthAddress).toBe( + args.antiAbuseOracleEthAddress.toLowerCase() + ) + }) + + it('encodes the AAO attestation instruction data correctly', () => { + const args = { + disbursementId: 'track-upload:1', + recipientEthAddress: '0x055FA758c77D68a04990E992aA4dcdeF899F654A', + amount: BigInt(123) + } + const data = RewardManagerProgram.encodeAttestation(args) + expect(data.subarray(0, 20).toString('hex')).toBe( + args.recipientEthAddress.substring(2).toLowerCase() + ) + expect(data.subarray(20, 21).toString('utf-8')).toBe('_') + expect(data.subarray(21, 29).readBigInt64LE()).toBe(args.amount) + expect(data.subarray(29, 30).toString('utf-8')).toBe('_') + expect(data.subarray(30, 44).toString('utf-8')).toBe(args.disbursementId) + }) + + it('decodes the AAO attestation instruction data correctly', () => { + const args = { + disbursementId: 'track-upload:1', + recipientEthAddress: '0x055FA758c77D68a04990E992aA4dcdeF899F654A', + amount: BigInt(123) + } + const data = Buffer.from([ + 5, 95, 167, 88, 199, 125, 104, 160, 73, 144, 233, 146, 170, 77, 205, 239, + 137, 159, 101, 74, 95, 123, 0, 0, 0, 0, 0, 0, 0, 95, 116, 114, 97, 99, + 107, 45, 117, 112, 108, 111, 97, 100, 58, 49 + ]) + const attestation = RewardManagerProgram.decodeAttestation(data) + expect(attestation.recipientEthAddress).toBe( + args.recipientEthAddress.toLowerCase() + ) + expect(attestation.amount).toBe(args.amount) + expect(attestation.disbursementId).toBe(args.disbursementId) + expect(attestation.antiAbuseOracleEthAddress).toBe(null) + }) + + it('decodes the account data', () => { + const data = Buffer.from([ + 1, 182, 193, 28, 253, 102, 169, 6, 208, 160, 135, 219, 13, 183, 183, 115, + 130, 16, 205, 49, 82, 187, 88, 76, 117, 96, 175, 210, 205, 23, 16, 17, 91, + 1, 240, 213, 188, 24, 66, 31, 160, 77, 10, 42, 46, 245, 64, 186, 90, 159, + 4, 1, 75, 227, 52, 1, 125, 122, 189, 249, 224, 203, 195, 55, 152, 10, 225, + 118, 62, 10, 73, 119, 125, 247, 95, 0, 225, 245, 5, 0, 0, 0, 0, 95, 116, + 114, 97, 99, 107, 45, 117, 112, 108, 111, 97, 100, 58, 52, 56, 51, 49, 49, + 55, 52, 51, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 133, 95, 167, 88, 199, 125, 104, 160, 73, 144, 233, + 146, 170, 77, 205, 239, 137, 159, 101, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]) + const decoded = RewardManagerProgram.decodeAttestationsAccountData(data) + expect(decoded.messages[0].attestation.recipientEthAddress).toBe( + '0x34017d7abdf9e0cbc337980ae1763e0a49777df7' + ) + expect(decoded.messages[0].attestation.amount).toBe(BigInt(100000000)) + expect(decoded.messages[0].attestation.disbursementId).toBe( + 'track-upload:483117431' + ) + expect(decoded.messages[0].attestation.antiAbuseOracleEthAddress).toBe(null) + expect(decoded.messages.length === 1) + }) + + it('encodes the evaluate attestation instruction data', () => { + const mockPubkey = new PublicKey( + '7c7wdSMAvswavryV6d9knEskoptUx919F2bLFYPrffqQ' + ) + const instruction = + RewardManagerProgram.createEvaluateAttestationsInstruction({ + disbursementId: 'track-upload:483117431', + recipientEthAddress: '0x34017d7abdf9e0cbc337980ae1763e0a49777df7', + amount: BigInt('100000000'), + + rewardManagerState: mockPubkey, + attestations: mockPubkey, + antiAbuseOracle: mockPubkey, + authority: mockPubkey, + rewardManagerTokenSource: mockPubkey, + destinationUserBank: mockPubkey, + disbursementAccount: mockPubkey, + payer: mockPubkey + }) + expect(instruction.data.subarray(0, 1).readUint8()).toBe(7) + expect(instruction.data.subarray(1, 9).readBigInt64LE()).toBe( + BigInt('100000000') + ) + expect(instruction.data.subarray(9).readUint16LE()).toBe( + instruction.data.byteLength - 20 - 9 - 4 // ethAddress, curOffset, size of this number + ) + expect( + instruction.data + .subarray(13, instruction.data.byteLength - 20) + .toString('utf-8') + ).toBe('track-upload:483117431') + expect( + instruction.data + .subarray(instruction.data.byteLength - 20) + .toString('hex') + ).toBe('34017d7abdf9e0cbc337980ae1763e0a49777df7') + expect(instruction.data.byteLength).toBe(55) + }) +}) diff --git a/packages/spl/src/reward-manager/RewardManagerProgram.ts b/packages/spl/src/reward-manager/RewardManagerProgram.ts new file mode 100644 index 00000000000..55349df5e78 --- /dev/null +++ b/packages/spl/src/reward-manager/RewardManagerProgram.ts @@ -0,0 +1,610 @@ +import { seq, struct, u8 } from '@solana/buffer-layout' +import { publicKey, u64 } from '@solana/buffer-layout-utils' +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { + AccountMeta, + PublicKey, + SYSVAR_INSTRUCTIONS_PUBKEY, + SYSVAR_RENT_PUBKEY, + SystemProgram, + TransactionInstruction +} from '@solana/web3.js' + +import { borshString, ethAddress } from '../layout-utils' + +import { attestationLayout } from './AttestationLayout' +import { RewardManagerInstruction } from './constants' +import { + Attestation, + CreateRewardSenderParams, + CreateRewardSenderPublicParams, + CreateSenderInstructionData, + CreateSenderPublicInstructionData, + DecodedCreateSenderInstruction, + DecodedCreateSenderPublicInstruction, + DecodedDeleteSenderPublicInstruction, + DecodedEvaluateAttestationsInstruction, + DecodedRewardManagerInstruction, + DecodedSubmitAttestationsInstruction, + EvaluateAttestationsInstructionData, + EvaluateRewardAttestationsParams, + RewardManagerStateData, + SubmitAttestationInstructionData, + SubmitRewardAttestationParams, + VerifiedMessage, + AttestationsAccountData +} from './types' + +const encoder = new TextEncoder() +const SENDER_SEED_PREFIX = 'S_' +const SENDER_SEED_PREFIX_BYTES = encoder.encode(SENDER_SEED_PREFIX) +const ATTESTATIONS_SEED_PREFIX = 'V_' +const ATTESTATIONS_SEED_PREFIX_BYTES = encoder.encode(ATTESTATIONS_SEED_PREFIX) +const DISBURSEMENT_SEED_PREFIX = 'T_' +const DISBURSEMENT_SEED_PREFIX_BYTES = encoder.encode(DISBURSEMENT_SEED_PREFIX) + +export class RewardManagerProgram { + public static readonly programId = new PublicKey( + 'DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei' + ) + + public static readonly layouts = { + createSenderInstructionData: struct([ + u8('instruction'), + ethAddress('senderEthAddress'), + ethAddress('operatorEthAddress') + ]), + createSenderPublicInstructionData: + struct([ + u8('instruction'), + ethAddress('senderEthAddress'), + ethAddress('operatorEthAddress') + ]), + evaluateAttestationsInstructionData: + struct([ + u8('instruction'), + u64('amount'), + borshString(32, 'disbursementId'), + ethAddress('recipientEthAddress') + ]), + submitAttestationInstructionData: struct([ + u8('instruction'), + borshString(32, 'disbursementId') + ]), + rewardManagerStateData: struct([ + u8('version'), + publicKey('tokenAccount'), + publicKey('manager'), + u8('minVotes') + ]), + attestationsAccountData: struct([ + u8('version'), + publicKey('rewardManagerState'), + seq( + struct([ + u8('index'), + ethAddress('senderEthAddress'), + attestationLayout('attestation'), + ethAddress('operator') + ]), + 3, + 'messages' + ) + ]) + } + + public static createSenderInstruction({ + senderEthAddress, + operatorEthAddress, + rewardManagerState, + manager, + authority, + payer, + sender, + rewardManagerProgramId = RewardManagerProgram.programId + }: CreateRewardSenderParams) { + const data = Buffer.alloc( + RewardManagerProgram.layouts.createSenderInstructionData.span + ) + RewardManagerProgram.layouts.createSenderInstructionData.encode( + { + instruction: RewardManagerInstruction.CreateSender, + senderEthAddress, + operatorEthAddress + }, + data + ) + const keys: AccountMeta[] = [ + { pubkey: rewardManagerState, isSigner: false, isWritable: false }, + { pubkey: manager, isSigner: true, isWritable: false }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: false }, + { pubkey: sender, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false } + ] + return new TransactionInstruction({ + programId: rewardManagerProgramId, + keys, + data + }) + } + + public static decodeCreateSenderInstruction({ + programId, + keys: [ + rewardManagerState, + manager, + authority, + payer, + sender, + systemProgramId, + rent + ], + data + }: TransactionInstruction): DecodedCreateSenderInstruction { + return { + programId, + keys: { + rewardManagerState, + manager, + authority, + payer, + sender, + systemProgramId, + rent + }, + data: RewardManagerProgram.layouts.createSenderInstructionData.decode( + data + ) + } + } + + public static createSenderPublicInstruction({ + senderEthAddress, + operatorEthAddress, + rewardManagerState, + authority, + payer, + sender, + existingSenders, + rewardManagerProgramId + }: CreateRewardSenderPublicParams) { + const data = Buffer.alloc( + RewardManagerProgram.layouts.createSenderPublicInstructionData.span + ) + RewardManagerProgram.layouts.createSenderPublicInstructionData.encode( + { + instruction: RewardManagerInstruction.CreateSenderPublic, + senderEthAddress, + operatorEthAddress + }, + data + ) + const keys = [ + { pubkey: rewardManagerState, isSigner: false, isWritable: false }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: sender, isSigner: false, isWritable: true }, + { + pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, + isSigner: false, + isWritable: false + }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ...existingSenders.map((pubkey) => ({ + pubkey, + isSigner: true, + isWritable: false + })) + ] + return new TransactionInstruction({ + programId: rewardManagerProgramId, + keys, + data + }) + } + + public static decodeCreateSenderPublicInstruction({ + programId, + keys: [ + rewardManagerState, + authority, + payer, + sender, + sysvarInstructions, + rent, + systemProgramId, + ...existingSenders + ], + data + }: TransactionInstruction): DecodedCreateSenderPublicInstruction { + return { + programId, + keys: { + rewardManagerState, + authority, + payer, + sender, + sysvarInstructions, + rent, + systemProgramId, + existingSenders + }, + data: RewardManagerProgram.layouts.createSenderPublicInstructionData.decode( + data + ) + } + } + + public static decodeDeleteSenderPublicInstruction({ + programId, + keys: [ + rewardManagerState, + sender, + refunder, + sysvarInstructions, + ...existingSenders + ] + }: TransactionInstruction): DecodedDeleteSenderPublicInstruction { + return { + programId, + keys: { + rewardManagerState, + sender, + refunder, + sysvarInstructions, + existingSenders + }, + data: { + instruction: RewardManagerInstruction.DeleteSenderPublic + } + } + } + + public static createSubmitAttestationInstruction = ({ + disbursementId, + attestations, + rewardManagerState, + authority, + payer, + sender, + rewardManagerProgramId = RewardManagerProgram.programId + }: SubmitRewardAttestationParams) => { + const b = Buffer.alloc(this.layouts.submitAttestationInstructionData.span) + const length = this.layouts.submitAttestationInstructionData.encode( + { + instruction: RewardManagerInstruction.SubmitAttestation, + disbursementId + }, + b + ) + const data = b.subarray(0, length) + const keys: AccountMeta[] = [ + { pubkey: attestations, isSigner: false, isWritable: true }, + { pubkey: rewardManagerState, isSigner: false, isWritable: false }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: sender, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, + { + pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, + isSigner: false, + isWritable: false + }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false } + ] + return new TransactionInstruction({ + programId: rewardManagerProgramId, + keys, + data + }) + } + + public static decodeSubmitAttestationInstruction({ + programId, + keys: [ + attestations, + rewardManagerState, + authority, + payer, + sender, + rent, + sysvarInstructions, + systemProgramId + ], + data + }: TransactionInstruction): DecodedSubmitAttestationsInstruction { + return { + programId, + keys: { + attestations, + rewardManagerState, + authority, + payer, + sender, + rent, + sysvarInstructions, + systemProgramId + }, + data: RewardManagerProgram.layouts.submitAttestationInstructionData.decode( + data + ) + } + } + + public static createEvaluateAttestationsInstruction = ({ + disbursementId, + recipientEthAddress, + amount, + attestations, + rewardManagerState, + authority, + rewardManagerTokenSource, + destinationUserBank, + disbursementAccount, + antiAbuseOracle, + payer, + tokenProgramId = TOKEN_PROGRAM_ID, + rewardManagerProgramId = RewardManagerProgram.programId + }: EvaluateRewardAttestationsParams) => { + const b = Buffer.alloc( + RewardManagerProgram.layouts.evaluateAttestationsInstructionData.span + ) + const length = + RewardManagerProgram.layouts.evaluateAttestationsInstructionData.encode( + { + instruction: RewardManagerInstruction.EvaluateAttestations, + disbursementId, + amount, + recipientEthAddress + }, + b + ) + const data = b.subarray(0, length) + const keys: AccountMeta[] = [ + { pubkey: attestations, isSigner: false, isWritable: true }, + { pubkey: rewardManagerState, isSigner: false, isWritable: false }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: rewardManagerTokenSource, isSigner: false, isWritable: true }, + { pubkey: destinationUserBank, isSigner: false, isWritable: true }, + { pubkey: disbursementAccount, isSigner: false, isWritable: true }, + { pubkey: antiAbuseOracle, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, + { pubkey: tokenProgramId, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false } + ] + return new TransactionInstruction({ + programId: rewardManagerProgramId, + keys, + data + }) + } + + public static decodeEvaluateAttestationsInstruction({ + programId, + keys: [ + attestations, + rewardManagerState, + authority, + rewardManagerTokenSource, + destinationUserbank, + disbursementAccount, + antiAbuseOracle, + payer, + rent, + tokenProgramId, + systemProgramId + ], + data + }: TransactionInstruction): DecodedEvaluateAttestationsInstruction { + return { + programId, + keys: { + attestations, + rewardManagerState, + authority, + rewardManagerTokenSource, + destinationUserbank, + disbursementAccount, + antiAbuseOracle, + payer, + rent, + tokenProgramId, + systemProgramId + }, + data: RewardManagerProgram.layouts.evaluateAttestationsInstructionData.decode( + data + ) + } + } + + public static decodeInstruction( + instruction: TransactionInstruction + ): DecodedRewardManagerInstruction { + switch (instruction.data[0]) { + case RewardManagerInstruction.Init: + case RewardManagerInstruction.ChangeManagerAccount: + throw new Error('Not Implemented') + case RewardManagerInstruction.CreateSender: + return RewardManagerProgram.decodeCreateSenderInstruction(instruction) + case RewardManagerInstruction.DeleteSender: + throw new Error('Not Implemented') + case RewardManagerInstruction.CreateSenderPublic: + return RewardManagerProgram.decodeCreateSenderPublicInstruction( + instruction + ) + case RewardManagerInstruction.DeleteSenderPublic: + return RewardManagerProgram.decodeDeleteSenderPublicInstruction( + instruction + ) + case RewardManagerInstruction.SubmitAttestation: + return RewardManagerProgram.decodeSubmitAttestationInstruction( + instruction + ) + case RewardManagerInstruction.EvaluateAttestations: + return RewardManagerProgram.decodeEvaluateAttestationsInstruction( + instruction + ) + default: + throw new Error('Invalid RewardManager Instruction') + } + } + + public static isCreateSenderInstruction( + decoded: DecodedRewardManagerInstruction + ): decoded is DecodedCreateSenderInstruction { + return decoded.data.instruction === RewardManagerInstruction.CreateSender + } + + public static isCreateSenderPublicInstruction( + decoded: DecodedRewardManagerInstruction + ): decoded is DecodedCreateSenderPublicInstruction { + return ( + decoded.data.instruction === RewardManagerInstruction.CreateSenderPublic + ) + } + + public static isDeleteSenderPublicInstruction( + decoded: DecodedRewardManagerInstruction + ): decoded is DecodedDeleteSenderPublicInstruction { + return ( + decoded.data.instruction === RewardManagerInstruction.DeleteSenderPublic + ) + } + + public static isSubmitAttestationInstruction( + decoded: DecodedRewardManagerInstruction + ): decoded is DecodedSubmitAttestationsInstruction { + return ( + decoded.data.instruction === RewardManagerInstruction.SubmitAttestation + ) + } + + public static isEvaluateAttestationsInstruction( + decoded: DecodedRewardManagerInstruction + ): decoded is DecodedEvaluateAttestationsInstruction { + return ( + decoded.data.instruction === RewardManagerInstruction.EvaluateAttestations + ) + } + + public static encodeAttestation(attestation: Attestation) { + const data = Buffer.alloc(attestationLayout().span) + const span = attestationLayout().encode(attestation, data) + return data.subarray(0, span) + } + + public static decodeAttestation(data: Buffer | Uint8Array) { + return attestationLayout().decode(data) + } + + public static decodeAttestationsAccountData(data: Buffer | Uint8Array) { + const decoded = this.layouts.attestationsAccountData.decode(data) + // decoded.messages = decoded.messages.filter((m) => m.index !== 0) + for (let i = 0; i < decoded.messages.length; i++) { + if ( + decoded.messages[i].attestation.antiAbuseOracleEthAddress === + '0x0000000000000000000000000000000000000000' + ) { + decoded.messages[i].attestation.antiAbuseOracleEthAddress = null + } + } + return decoded + } + + public static deriveAuthority({ + programId, + rewardManagerState + }: { + programId: PublicKey + rewardManagerState: PublicKey + }) { + return PublicKey.findProgramAddressSync( + [rewardManagerState.toBytes().slice(0, 32)], + programId + )[0] + } + + public static deriveSender({ + ethAddress: wallet, + programId, + authority + }: { + ethAddress: string + programId: PublicKey + authority: PublicKey + }) { + const ethAddressData = ethAddress(wallet) + const buffer = Buffer.alloc(ethAddressData.span) + ethAddressData.encode(wallet, buffer) + const seed = Uint8Array.from([...SENDER_SEED_PREFIX_BYTES, ...buffer]) + return PublicKey.findProgramAddressSync( + [authority.toBytes().slice(0, 32), seed], + programId + )[0] + } + + public static deriveAttestations({ + disbursementId, + programId, + authority + }: { + disbursementId: string + programId: PublicKey + authority: PublicKey + }) { + const encoder = new TextEncoder() + const seed = Uint8Array.from([ + ...ATTESTATIONS_SEED_PREFIX_BYTES, + ...encoder.encode(disbursementId) + ]) + return PublicKey.findProgramAddressSync( + [authority.toBytes().slice(0, 32), seed], + programId + )[0] + } + + public static deriveDisbursement({ + disbursementId, + programId, + authority + }: { + disbursementId: string + programId: PublicKey + authority: PublicKey + }) { + const encoder = new TextEncoder() + const seed = Uint8Array.from([ + ...DISBURSEMENT_SEED_PREFIX_BYTES, + ...encoder.encode(disbursementId) + ]) + return PublicKey.findProgramAddressSync( + [authority.toBytes().slice(0, 32), seed], + programId + )[0] + } + + public static encodeSignature(signature: string) { + const recoveryIdString = signature.slice(-2) + const recoveryIdBuffer = Buffer.from(recoveryIdString, 'hex') + const strippedSignature = signature + .substring(0, signature.length - 2) + .replace('0x', '') + const signatureBuffer = Buffer.from(strippedSignature, 'hex') + const fixedBuf = Buffer.alloc(64, 0) + signatureBuffer.copy(fixedBuf, 64 - signatureBuffer.length) + return { + signature: signatureBuffer, + recoveryId: recoveryIdBuffer.readInt8() + } + } + + public static decodeRewardManagerState(accountData: Uint8Array | Buffer) { + return RewardManagerProgram.layouts.rewardManagerStateData.decode( + accountData + ) + } +} diff --git a/packages/spl/src/reward-manager/constants.ts b/packages/spl/src/reward-manager/constants.ts index 809e0608d39..4c6055697ed 100644 --- a/packages/spl/src/reward-manager/constants.ts +++ b/packages/spl/src/reward-manager/constants.ts @@ -1,5 +1,3 @@ -import { PublicKey } from '@solana/web3.js' - export enum RewardManagerInstruction { Init = 0, ChangeManagerAccount = 1, @@ -10,7 +8,3 @@ export enum RewardManagerInstruction { SubmitAttestation = 6, EvaluateAttestations = 7 } - -export const REWARD_MANAGER_PROGRAM_ID = new PublicKey( - 'DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei' -) diff --git a/packages/spl/src/reward-manager/createSender.ts b/packages/spl/src/reward-manager/createSender.ts deleted file mode 100644 index 24dc60110c8..00000000000 --- a/packages/spl/src/reward-manager/createSender.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout' -import { - AccountMeta, - PublicKey, - SYSVAR_RENT_PUBKEY, - SystemProgram, - TransactionInstruction -} from '@solana/web3.js' - -import { ethAddress } from '../layout-utils' - -import { - REWARD_MANAGER_PROGRAM_ID, - RewardManagerInstruction -} from './constants' - -type CreateSenderInstructionData = { - instruction: RewardManagerInstruction - senderEthAddress: string - operatorEthAddress: string -} - -const createSenderInstructionData = struct([ - u8('instruction'), - ethAddress('senderEthAddress'), - ethAddress('operatorEthAddress') -]) - -export const createSenderInstruction = ( - senderEthAddress: string, - operatorEthAddress: string, - rewardManager: PublicKey, - owner: PublicKey, - authority: PublicKey, - payer: PublicKey, - sender: PublicKey, - rewardManagerProgramId: PublicKey = REWARD_MANAGER_PROGRAM_ID -) => { - const data = Buffer.alloc(createSenderInstructionData.span) - createSenderInstructionData.encode( - { - instruction: RewardManagerInstruction.CreateSender, - senderEthAddress, - operatorEthAddress - }, - data - ) - const keys: AccountMeta[] = [ - { pubkey: rewardManager, isSigner: false, isWritable: false }, - { pubkey: owner, isSigner: true, isWritable: false }, - { pubkey: authority, isSigner: false, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: false }, - { pubkey: sender, isSigner: false, isWritable: true }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false } - ] - return new TransactionInstruction({ - programId: rewardManagerProgramId, - keys, - data - }) -} - -export type DecodedCreateSenderInstruction = { - programId: PublicKey - keys: { - rewardManager: AccountMeta - owner: AccountMeta - authority: AccountMeta - payer: AccountMeta - sender: AccountMeta - systemProgramId: AccountMeta - rent: AccountMeta - } - data: CreateSenderInstructionData -} - -export const decodeCreateSenderInstruction = ({ - programId, - keys: [rewardManager, owner, authority, payer, sender, systemProgramId, rent], - data -}: TransactionInstruction): DecodedCreateSenderInstruction => ({ - programId, - keys: { - rewardManager, - owner, - authority, - payer, - sender, - systemProgramId, - rent - }, - data: createSenderInstructionData.decode(data) -}) diff --git a/packages/spl/src/reward-manager/createSenderPublic.ts b/packages/spl/src/reward-manager/createSenderPublic.ts deleted file mode 100644 index 8e2238fdc80..00000000000 --- a/packages/spl/src/reward-manager/createSenderPublic.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout' -import { - PublicKey, - AccountMeta, - TransactionInstruction, - SYSVAR_INSTRUCTIONS_PUBKEY, - SYSVAR_RENT_PUBKEY, - SystemProgram -} from '@solana/web3.js' - -import { ethAddress } from '../layout-utils' - -import { - REWARD_MANAGER_PROGRAM_ID, - RewardManagerInstruction -} from './constants' - -type CreateSenderPublicInstructionData = { - instruction: RewardManagerInstruction - senderEthAddress: string - operatorEthAddress: string -} - -const createSenderPublicInstructionData = - struct([ - u8('instruction'), - ethAddress('senderEthAddress'), - ethAddress('operatorEthAddress') - ]) - -export const createSenderPublicInstruction = ( - senderEthAddress: string, - operatorEthAddress: string, - rewardManager: PublicKey, - authority: PublicKey, - payer: PublicKey, - sender: PublicKey, - existingSenders: PublicKey[], - rewardManagerProgramId: PublicKey = REWARD_MANAGER_PROGRAM_ID -) => { - const data = Buffer.alloc(createSenderPublicInstructionData.span) - createSenderPublicInstructionData.encode( - { - instruction: RewardManagerInstruction.CreateSenderPublic, - senderEthAddress, - operatorEthAddress - }, - data - ) - const keys = [ - { pubkey: rewardManager, isSigner: false, isWritable: false }, - { pubkey: authority, isSigner: false, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: sender, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - ...existingSenders.map((pubkey) => ({ - pubkey, - isSigner: false, - isWritable: false - })) - ] - return new TransactionInstruction({ - programId: rewardManagerProgramId, - keys, - data - }) -} - -export type DecodedCreateSenderPublicInstruction = { - programId: PublicKey - keys: { - rewardManager: AccountMeta - authority: AccountMeta - payer: AccountMeta - sender: AccountMeta - sysvarInstructions: AccountMeta - rent: AccountMeta - systemProgramId: AccountMeta - existingSenders: AccountMeta[] - } - data: CreateSenderPublicInstructionData -} - -export const decodeCreateSenderPublicInstruction = ({ - programId, - keys: [ - rewardManager, - authority, - payer, - sender, - sysvarInstructions, - rent, - systemProgramId, - ...existingSenders - ], - data -}: TransactionInstruction): DecodedCreateSenderPublicInstruction => ({ - programId, - keys: { - rewardManager, - authority, - payer, - sender, - sysvarInstructions, - rent, - systemProgramId, - existingSenders - }, - data: createSenderPublicInstructionData.decode(data) -}) diff --git a/packages/spl/src/reward-manager/decode.ts b/packages/spl/src/reward-manager/decode.ts deleted file mode 100644 index e399aa329c1..00000000000 --- a/packages/spl/src/reward-manager/decode.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { TransactionInstruction } from '@solana/web3.js' - -import { RewardManagerInstruction } from './constants' -import { - DecodedCreateSenderInstruction, - decodeCreateSenderInstruction -} from './createSender' -import { - DecodedCreateSenderPublicInstruction, - decodeCreateSenderPublicInstruction -} from './createSenderPublic' -import { - DecodedDeleteSenderPublicInstruction, - decodeDeleteSenderPublicInstruction -} from './deleteSenderPublic' -import { - DecodedEvaluateAttestationsInstruction, - decodeEvaluateAttestationsInstruction -} from './evaluateAttestations' -import { - DecodedSubmitAttestationsInstruction, - decodeSubmitAttestationInstruction -} from './submitAttestation' - -type DecodedRewardManagerInstruction = - | DecodedCreateSenderInstruction - | DecodedCreateSenderPublicInstruction - | DecodedDeleteSenderPublicInstruction - | DecodedSubmitAttestationsInstruction - | DecodedEvaluateAttestationsInstruction - -export const decodeRewardManagerInstruction = ( - instruction: TransactionInstruction -): DecodedRewardManagerInstruction => { - switch (instruction.data[0]) { - case RewardManagerInstruction.Init: - case RewardManagerInstruction.ChangeManagerAccount: - throw new Error('Not Implemented') - case RewardManagerInstruction.CreateSender: - return decodeCreateSenderInstruction(instruction) - case RewardManagerInstruction.DeleteSender: - throw new Error('Not Implemented') - case RewardManagerInstruction.CreateSenderPublic: - return decodeCreateSenderPublicInstruction(instruction) - case RewardManagerInstruction.DeleteSenderPublic: - return decodeDeleteSenderPublicInstruction(instruction) - case RewardManagerInstruction.SubmitAttestation: - return decodeSubmitAttestationInstruction(instruction) - case RewardManagerInstruction.EvaluateAttestations: - return decodeEvaluateAttestationsInstruction(instruction) - default: - throw new Error('Invalid RewardManager Instruction') - } -} diff --git a/packages/spl/src/reward-manager/deleteSenderPublic.ts b/packages/spl/src/reward-manager/deleteSenderPublic.ts deleted file mode 100644 index e2704a06185..00000000000 --- a/packages/spl/src/reward-manager/deleteSenderPublic.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PublicKey, AccountMeta, TransactionInstruction } from '@solana/web3.js' - -import { RewardManagerInstruction } from './constants' - -export type DecodedDeleteSenderPublicInstruction = { - programId: PublicKey - keys: { - rewardManager: AccountMeta - sender: AccountMeta - refunder: AccountMeta - sysvarInstructions: AccountMeta - existingSenders: AccountMeta[] - } - data: { - instruction: RewardManagerInstruction.DeleteSenderPublic - } -} - -export const decodeDeleteSenderPublicInstruction = ({ - programId, - keys: [ - rewardManager, - sender, - refunder, - sysvarInstructions, - ...existingSenders - ] -}: TransactionInstruction): DecodedDeleteSenderPublicInstruction => ({ - programId, - keys: { - rewardManager, - sender, - refunder, - sysvarInstructions, - existingSenders - }, - data: { - instruction: RewardManagerInstruction.DeleteSenderPublic - } -}) diff --git a/packages/spl/src/reward-manager/evaluateAttestations.ts b/packages/spl/src/reward-manager/evaluateAttestations.ts deleted file mode 100644 index 3db85bebed6..00000000000 --- a/packages/spl/src/reward-manager/evaluateAttestations.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout' -import { u64 } from '@solana/buffer-layout-utils' -import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { - PublicKey, - AccountMeta, - TransactionInstruction, - SYSVAR_RENT_PUBKEY, - SystemProgram -} from '@solana/web3.js' - -import { borshString, ethAddress } from '../layout-utils' - -import { - REWARD_MANAGER_PROGRAM_ID, - RewardManagerInstruction -} from './constants' - -type EvaluateAttestationsInstructionData = { - instruction: RewardManagerInstruction - amount: bigint - transferId: string - destinationEthAddress: string -} - -const evaluateAttestationsInstructionData = - struct([ - u8('instruction'), - u64('amount'), - borshString(32, 'transferId'), - ethAddress('ethRecipient') - ]) - -export const createEvaluateAttestationsInstruction = ( - transferId: string, - destinationEthAddress: string, - amount: bigint, - verifiedMessages: PublicKey, - rewardManager: PublicKey, - authority: PublicKey, - rewardManagerTokenSource: PublicKey, - destinationUserbank: PublicKey, - transferAccount: PublicKey, - antiAbuse: PublicKey, - payer: PublicKey, - tokenProgramId: PublicKey = TOKEN_PROGRAM_ID, - rewardManagerProgramId: PublicKey = REWARD_MANAGER_PROGRAM_ID -) => { - const maxData = Buffer.alloc(evaluateAttestationsInstructionData.span) - const actualLength = evaluateAttestationsInstructionData.encode( - { - instruction: RewardManagerInstruction.EvaluateAttestations, - transferId, - amount, - destinationEthAddress - }, - maxData - ) - const data = maxData.slice(0, actualLength) - const keys: AccountMeta[] = [ - { pubkey: verifiedMessages, isSigner: false, isWritable: true }, - { pubkey: rewardManager, isSigner: false, isWritable: false }, - { pubkey: authority, isSigner: false, isWritable: false }, - { pubkey: rewardManagerTokenSource, isSigner: false, isWritable: true }, - { pubkey: destinationUserbank, isSigner: false, isWritable: true }, - { pubkey: transferAccount, isSigner: false, isWritable: true }, - { pubkey: antiAbuse, isSigner: false, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: tokenProgramId, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false } - ] - return new TransactionInstruction({ - programId: rewardManagerProgramId, - keys, - data - }) -} - -export type DecodedEvaluateAttestationsInstruction = { - programId: PublicKey - keys: { - verifiedMessages: AccountMeta - rewardManager: AccountMeta - authority: AccountMeta - rewardManagerTokenSource: AccountMeta - destinationUserbank: AccountMeta - transferAccount: AccountMeta - antiAbuse: AccountMeta - payer: AccountMeta - rent: AccountMeta - tokenProgramId: AccountMeta - systemProgramId: AccountMeta - } - data: EvaluateAttestationsInstructionData -} - -export const decodeEvaluateAttestationsInstruction = ({ - programId, - keys: [ - verifiedMessages, - rewardManager, - authority, - rewardManagerTokenSource, - destinationUserbank, - transferAccount, - antiAbuse, - payer, - rent, - tokenProgramId, - systemProgramId - ], - data -}: TransactionInstruction): DecodedEvaluateAttestationsInstruction => ({ - programId, - keys: { - verifiedMessages, - rewardManager, - authority, - rewardManagerTokenSource, - destinationUserbank, - transferAccount, - antiAbuse, - payer, - rent, - tokenProgramId, - systemProgramId - }, - data: evaluateAttestationsInstructionData.decode(data) -}) diff --git a/packages/spl/src/reward-manager/index.ts b/packages/spl/src/reward-manager/index.ts deleted file mode 100644 index 08b8956953e..00000000000 --- a/packages/spl/src/reward-manager/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './constants' -export * from './decode' -export * from './createSender' -export * from './createSenderPublic' -export * from './deleteSenderPublic' -export * from './evaluateAttestations' -export * from './submitAttestation' diff --git a/packages/spl/src/reward-manager/submitAttestation.ts b/packages/spl/src/reward-manager/submitAttestation.ts deleted file mode 100644 index 706815b645f..00000000000 --- a/packages/spl/src/reward-manager/submitAttestation.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout' -import { - PublicKey, - AccountMeta, - TransactionInstruction, - SYSVAR_RENT_PUBKEY, - SYSVAR_INSTRUCTIONS_PUBKEY, - SystemProgram -} from '@solana/web3.js' - -import { borshString } from '../layout-utils' - -import { - REWARD_MANAGER_PROGRAM_ID, - RewardManagerInstruction -} from './constants' - -type SubmitAttestationInstructionData = { - instruction: RewardManagerInstruction - transferId: string -} -const submitAttestationInstructionData = - struct([ - u8('instruction'), - borshString(32, 'transferId') - ]) - -export const createSubmitAttestationInstruction = ( - transferId: string, - verifiedMessages: PublicKey, - rewardManager: PublicKey, - authority: PublicKey, - payer: PublicKey, - sender: PublicKey, - rewardManagerProgramId: PublicKey = REWARD_MANAGER_PROGRAM_ID -) => { - const largerData = Buffer.alloc(submitAttestationInstructionData.span) - const actualLength = submitAttestationInstructionData.encode( - { instruction: RewardManagerInstruction.SubmitAttestation, transferId }, - largerData - ) - const data = largerData.slice(0, actualLength) - const keys: AccountMeta[] = [ - { pubkey: verifiedMessages, isSigner: false, isWritable: true }, - { pubkey: rewardManager, isSigner: false, isWritable: false }, - { pubkey: authority, isSigner: false, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: sender, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false } - ] - return new TransactionInstruction({ - programId: rewardManagerProgramId, - keys, - data - }) -} - -export type DecodedSubmitAttestationsInstruction = { - programId: PublicKey - keys: { - verifiedMessages: AccountMeta - rewardManager: AccountMeta - authority: AccountMeta - payer: AccountMeta - sender: AccountMeta - rent: AccountMeta - sysvarInstructions: AccountMeta - systemProgramId: AccountMeta - } - data: { instruction: RewardManagerInstruction; transferId: string } -} - -export const decodeSubmitAttestationInstruction = ({ - programId, - keys: [ - verifiedMessages, - rewardManager, - authority, - payer, - sender, - rent, - sysvarInstructions, - systemProgramId - ], - data -}: TransactionInstruction): DecodedSubmitAttestationsInstruction => ({ - programId, - keys: { - verifiedMessages, - rewardManager, - authority, - payer, - sender, - rent, - sysvarInstructions, - systemProgramId - }, - data: submitAttestationInstructionData.decode(data) -}) diff --git a/packages/spl/src/reward-manager/types.ts b/packages/spl/src/reward-manager/types.ts new file mode 100644 index 00000000000..dd9d46f98be --- /dev/null +++ b/packages/spl/src/reward-manager/types.ts @@ -0,0 +1,297 @@ +import { AccountMeta, PublicKey } from '@solana/web3.js' + +import { RewardManagerInstruction } from './constants' + +export type CreateRewardSenderParams = { + /** The node's Ethereum wallet address. */ + senderEthAddress: string + /** The node operator's Ethereum wallet address. */ + operatorEthAddress: string + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: PublicKey + /** The admin account that controls the reward manager state. */ + manager: PublicKey + /** The PDA that owns the derived sender accounts. */ + authority: PublicKey + /** The account used to pay for the sender account creation. */ + payer: PublicKey + /** The sender account to create. */ + sender: PublicKey + /** The programId of the Reward Manager Program. */ + rewardManagerProgramId: PublicKey +} + +export type CreateSenderInstructionData = { + /** The instruction identifier. */ + instruction: RewardManagerInstruction + /** The node's Ethereum wallet address. */ + senderEthAddress: string + /** The node operator's Ethereum wallet address. */ + operatorEthAddress: string +} + +export type DecodedCreateSenderInstruction = { + programId: PublicKey + keys: { + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: AccountMeta + /** The admin account that controls the reward manager state. */ + manager: AccountMeta + /** The PDA that owns the derived sender accounts. */ + authority: AccountMeta + /** The account used to pay for the sender account creation. */ + payer: AccountMeta + /** The sender account to create. */ + sender: AccountMeta + /** The programId of System Program. */ + systemProgramId: AccountMeta + /** The rent sysvar account. */ + rent: AccountMeta + } + data: CreateSenderInstructionData +} + +export type CreateRewardSenderPublicParams = { + /** The node's Ethereum wallet address. */ + senderEthAddress: string + /** The node operator's Ethereum wallet address. */ + operatorEthAddress: string + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: PublicKey + /** The PDA that owns the derived sender accounts. */ + authority: PublicKey + /** The account used to pay for the sender account creation. */ + payer: PublicKey + /** The sender account to create. */ + sender: PublicKey + /** A list of existing sender accounts that attest to the new sender. */ + existingSenders: PublicKey[] + /** The programId of the Reward Manager Program. */ + rewardManagerProgramId: PublicKey +} + +export type CreateSenderPublicInstructionData = { + /** The instruction identifier. */ + instruction: RewardManagerInstruction + /** The node's Ethereum wallet address. */ + senderEthAddress: string + /** The node operator's Ethereum wallet address. */ + operatorEthAddress: string +} + +export type DecodedCreateSenderPublicInstruction = { + programId: PublicKey + keys: { + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: AccountMeta + /** The PDA that owns the derived sender accounts. */ + authority: AccountMeta + /** The account used to pay for the sender account creation. */ + payer: AccountMeta + /** The sender account to create. */ + sender: AccountMeta + /** The instructions sysvar account. */ + sysvarInstructions: AccountMeta + /** The rent sysvar account. */ + rent: AccountMeta + /** The programId of System Program. */ + systemProgramId: AccountMeta + /** A list of existing sender accounts that attest to the new sender. */ + existingSenders: AccountMeta[] + } + data: CreateSenderPublicInstructionData +} + +export type DecodedDeleteSenderPublicInstruction = { + programId: PublicKey + keys: { + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: AccountMeta + /** The sender account to delete. */ + sender: AccountMeta + /** The account to refund the rent to from the sender account creation. */ + refunder: AccountMeta + /** The instructions sysvar account. */ + sysvarInstructions: AccountMeta + /** A list of existing sender accounts that attest to the new sender. */ + existingSenders: AccountMeta[] + } + data: { + /** The instruction identifier. */ + instruction: RewardManagerInstruction.DeleteSenderPublic + } +} + +export type SubmitRewardAttestationParams = { + /** The identifier of a disbursement, usually "challengeId:specifier". */ + disbursementId: string + /** The derived account of the transferId that will hold the attestations. */ + attestations: PublicKey + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: PublicKey + /** The PDA that owns the derived sender accounts. */ + authority: PublicKey + /** The account used to pay for the sender account creation. */ + payer: PublicKey + /** The sender attesting. */ + sender: PublicKey + /** The programId of the Reward Manager Program. */ + rewardManagerProgramId: PublicKey +} + +export type SubmitAttestationInstructionData = { + /** The instruction identifier. */ + instruction: RewardManagerInstruction + /** The identifier of a disbursement, usually "challengeId:specifier". */ + disbursementId: string +} + +export type SubmitDiscoveryAttestationSignedInstructionData = { + /** The Ethereum wallet of the recipient. */ + recipientEthAddress: string + /** The amount to reward the recipient. */ + amount: bigint + /** The identifier of a disbursement, usually "challengeId:specifier". */ + disbursementId: string + /** The Ethereum wallet of the attesting anti abuse oracle. */ + antiAbuseOracleEthAddress: string +} + +export type SubmitOracleAttestationSignedInstructionData = { + /** The Ethereum wallet of the recipient. */ + recipientEthAddress: string + /** The amount to reward the recipient. */ + amount: bigint + /** The identifier of a disbursement, usually "challengeId:specifier". */ + disbursementId: string +} + +export type DecodedSubmitAttestationsInstruction = { + programId: PublicKey + keys: { + /** The derived account of the transferId that will hold the attestations. */ + attestations: AccountMeta + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: AccountMeta + /** The PDA that owns the derived sender accounts. */ + authority: AccountMeta + /** The account used to pay for the attestations account. */ + payer: AccountMeta + /** The sender attesting. */ + sender: AccountMeta + /** The rent sysvar account. */ + rent: AccountMeta + /** The instructions sysvar account. */ + sysvarInstructions: AccountMeta + /** The programId of System Program. */ + systemProgramId: AccountMeta + } + data: SubmitAttestationInstructionData +} + +export type EvaluateRewardAttestationsParams = { + /** The identifier of a disbursement, usually "challengeId:specifier". */ + disbursementId: string + /** The Ethereum wallet of the recipient of the disbursement. */ + recipientEthAddress: string + /** The amount to reward the recipient. */ + amount: bigint + /** The derived account of the transferId that holds the attestations to be evaluated. */ + attestations: PublicKey + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: PublicKey + /** The PDA that owns the derived sender accounts. */ + authority: PublicKey + /** The PDA that owns the reward tokens. */ + rewardManagerTokenSource: PublicKey + /** The user bank of the recipient. */ + destinationUserBank: PublicKey + /** The disbursement account that will mark that a disbursement has been completed. */ + disbursementAccount: PublicKey + /** The derived sender account for the attesting anti abuse oracle. */ + antiAbuseOracle: PublicKey + /** The account used to pay for the disbursement account creation and receive the attestations account rent. */ + payer: PublicKey + /** The programId of the SPL Token Program. */ + tokenProgramId?: PublicKey + /** The programId of the Reward Manager Program. */ + rewardManagerProgramId?: PublicKey +} + +export type EvaluateAttestationsInstructionData = { + /** The instruction identifier. */ + instruction: RewardManagerInstruction + /** The amount to reward the recipient. */ + amount: bigint + /** The identifier of a disbursement, usually "challengeId:specifier". */ + disbursementId: string + /** The Ethereum wallet of the recipient of the disbursement. */ + recipientEthAddress: string +} + +export type DecodedEvaluateAttestationsInstruction = { + programId: PublicKey + keys: { + /** The derived account of the transferId that holds the attestations to be evaluated. */ + attestations: AccountMeta + /** The PDA tracking the program state (version, token account with rewards, authority, and min votes) */ + rewardManagerState: AccountMeta + /** The PDA that owns the derived sender accounts. */ + authority: AccountMeta + /** The PDA that owns the reward tokens. */ + rewardManagerTokenSource: AccountMeta + /** The user bank of the recipient. */ + destinationUserbank: AccountMeta + /** The disbursement account that will mark that a disbursement has been completed. */ + disbursementAccount: AccountMeta + /** The derived sender account for the attesting anti abuse oracle. */ + antiAbuseOracle: AccountMeta + /** The account used to pay for the disbursement account creation and receive the attestations account rent. */ + payer: AccountMeta + /** The rent sysvar account. */ + rent: AccountMeta + /** The programId of the SPL Token Program. */ + tokenProgramId: AccountMeta + /** The programId of System Program. */ + systemProgramId: AccountMeta + } + data: EvaluateAttestationsInstructionData +} + +export type DecodedRewardManagerInstruction = + | DecodedCreateSenderInstruction + | DecodedCreateSenderPublicInstruction + | DecodedDeleteSenderPublicInstruction + | DecodedSubmitAttestationsInstruction + | DecodedEvaluateAttestationsInstruction + +export type RewardManagerStateData = { + /** The version of the program. */ + version: number + /** The PDA that owns the reward tokens. */ + tokenAccount: PublicKey + /** The admin account that controls the reward manager state. */ + manager: PublicKey + /** The minimum number of votes from attesters to achieve quorum. */ + minVotes: number +} + +export type Attestation = { + recipientEthAddress: string + amount: bigint + disbursementId: string + antiAbuseOracleEthAddress?: string | null +} + +export type VerifiedMessage = { + index: number + senderEthAddress: string + attestation: Attestation + operator: string +} + +export type AttestationsAccountData = { + version: number + rewardManagerState: PublicKey + messages: VerifiedMessage[] +} From 2959b8d7cf22f1ac7f060a56955a76fda812dd23 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:26:44 -0800 Subject: [PATCH 10/66] consume changes to audius/spl rewardmanager --- .../src/routes/relay/assertRelayAllowedInstructions.ts | 7 ++++--- .../src/typed-routes/solana/solanaRelayChecks.ts | 10 ++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts index e4422f39881..2a516177d7c 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts @@ -19,7 +19,7 @@ import { InvalidRelayInstructionError } from './InvalidRelayInstructionError' import { decodeAssociatedTokenAccountInstruction, ClaimableTokensProgram, - decodeRewardManagerInstruction, + RewardManagerProgram, isCreateAssociatedTokenAccountIdempotentInstruction, isCreateAssociatedTokenAccountInstruction } from '@audius/spl' @@ -178,8 +178,9 @@ const assertAllowedRewardsManagerProgramInstruction = ( instructionIndex: number, instruction: TransactionInstruction ) => { - const decodedInstruction = decodeRewardManagerInstruction(instruction) - const rewardManager = decodedInstruction.keys.rewardManager.pubkey.toBase58() + const decodedInstruction = RewardManagerProgram.decodeInstruction(instruction) + const rewardManager = + decodedInstruction.keys.rewardManagerState.pubkey.toBase58() if (rewardManager !== REWARD_MANAGER) { throw new InvalidRelayInstructionError( instructionIndex, diff --git a/packages/identity-service/src/typed-routes/solana/solanaRelayChecks.ts b/packages/identity-service/src/typed-routes/solana/solanaRelayChecks.ts index 19f941e03db..83ac2febc55 100644 --- a/packages/identity-service/src/typed-routes/solana/solanaRelayChecks.ts +++ b/packages/identity-service/src/typed-routes/solana/solanaRelayChecks.ts @@ -23,10 +23,7 @@ import { isCreateAssociatedTokenAccountInstruction, isCreateAssociatedTokenAccountIdempotentInstruction } from './programs/associatedToken' -import { - ClaimableTokensProgram, - decodeRewardManagerInstruction -} from '@audius/spl' +import { ClaimableTokensProgram, RewardManagerProgram } from '@audius/spl' import config from '../../config' const MEMO_PROGRAM_ID = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo' @@ -188,8 +185,9 @@ const assertAllowedRewardsManagerProgramInstruction = ( instructionIndex: number, instruction: TransactionInstruction ) => { - const decodedInstruction = decodeRewardManagerInstruction(instruction) - const rewardManager = decodedInstruction.keys.rewardManager.pubkey.toBase58() + const decodedInstruction = RewardManagerProgram.decodeInstruction(instruction) + const rewardManager = + decodedInstruction.keys.rewardManagerState.pubkey.toBase58() if (rewardManager !== REWARD_MANAGER) { throw new InvalidRelayInstructionError( instructionIndex, From 98ab0fa2a8deae75a489814d38a8dacc60d8f5c4 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:30:43 -0800 Subject: [PATCH 11/66] add AntiAbuseOracleSelector service and api --- .../api/antiAbuseOracle/AntiAbuseOracleApi.ts | 35 ++++++++++++ packages/libs/src/sdk/config/types.ts | 4 ++ .../src/sdk/scripts/generateServicesConfig.ts | 30 ++++++++-- packages/libs/src/sdk/sdk.ts | 4 ++ .../AntiAbuseOracleSelector.ts | 57 +++++++++++++++++++ .../AntiAbuseOracleSelector/constants.ts | 9 +++ .../services/AntiAbuseOracleSelector/index.ts | 2 + .../services/AntiAbuseOracleSelector/types.ts | 19 +++++++ 8 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracleSelector/index.ts create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts diff --git a/packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts b/packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts new file mode 100644 index 00000000000..9ca32b8ac68 --- /dev/null +++ b/packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts @@ -0,0 +1,35 @@ +import { BaseAPI, JSONApiResponse, exists } from '../generated/default' + +export class AntiAbuseOracleApi extends BaseAPI { + public async getChallengeAttestation({ + handle, + challengeId, + specifier, + amount + }: { + handle: string + challengeId: string + specifier: string + amount: number + }) { + const response = await this.request({ + path: `/attestation/${handle}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: { + challengeId, + challengeSpecifier: specifier, + amount + } + }) + return await new JSONApiResponse<{ + signature: string | false + errorCode?: number + }>(response, (json) => ({ + signature: exists(json, 'result') ? json.result : false, + errorCode: exists(json, 'errorCode') ? json.errorCode : undefined + })).value() + } +} diff --git a/packages/libs/src/sdk/config/types.ts b/packages/libs/src/sdk/config/types.ts index 6f19e3d39ee..be837a3d8dd 100644 --- a/packages/libs/src/sdk/config/types.ts +++ b/packages/libs/src/sdk/config/types.ts @@ -4,6 +4,10 @@ export type ServicesConfig = { minVersion: string discoveryNodes: string[] storageNodes: StorageNode[] + antiAbuseOracleNodes: { + endpoints: string[] + addresses: string[] + } entityManagerContractAddress: string web3ProviderUrl: string identityServiceUrl: string diff --git a/packages/libs/src/sdk/scripts/generateServicesConfig.ts b/packages/libs/src/sdk/scripts/generateServicesConfig.ts index 31ec0685a55..a3a1c5c1b0a 100644 --- a/packages/libs/src/sdk/scripts/generateServicesConfig.ts +++ b/packages/libs/src/sdk/scripts/generateServicesConfig.ts @@ -18,6 +18,7 @@ type EnvironmentConfig = { WORMHOLE_ADDRESS: string ENTITY_MANAGER_CONTRACT_ADDRESS: string WEB3_PROVIDER_URL: string + AAO_ENDPOINTS: string[] } const envConfigs: Record<'staging' | 'production', EnvironmentConfig> = { @@ -32,7 +33,12 @@ const envConfigs: Record<'staging' | 'production', EnvironmentConfig> = { WORMHOLE_ADDRESS: '0x6E7a1F7339bbB62b23D44797b63e4258d283E095', WEB3_PROVIDER_URL: 'https://poa-gateway.audius.co', ENTITY_MANAGER_CONTRACT_ADDRESS: - '0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64' + '0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64', + AAO_ENDPOINTS: [ + 'https://antiabuseoracle.audius.co', + 'https://audius-oracle.creatorseed.com', + 'https://oracle.audius.endl.net' + ] }, staging: { CLAIM_DISTRIBUTION_CONTRACT_ADDRESS: @@ -45,7 +51,8 @@ const envConfigs: Record<'staging' | 'production', EnvironmentConfig> = { WORMHOLE_ADDRESS: '0xf6f45e4d836da1d4ecd43bb1074620bfb0b7e0d7', WEB3_PROVIDER_URL: 'https://poa-gateway.staging.audius.co', ENTITY_MANAGER_CONTRACT_ADDRESS: - '0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64' + '0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64', + AAO_ENDPOINTS: ['https://antiabuseoracle.staging.audius.co'] } } @@ -60,7 +67,11 @@ const devConfig: ServicesConfig = { ], entityManagerContractAddress: '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B', web3ProviderUrl: 'http://audius-protocol-poa-ganache-1', - identityServiceUrl: 'http://audius-protocol-identity-service-1' + identityServiceUrl: 'http://audius-protocol-identity-service-1', + antiAbuseOracleNodes: { + endpoints: ['http://audius-protocol-anti-abuse-oracle-1:8000'], + addresses: ['0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3'] + } } const generateServicesConfig = async ( @@ -95,15 +106,25 @@ const generateServicesConfig = async ( if (!discoveryNodes || discoveryNodes.length === 0) { throw Error('Discovery node services not found') } + const storageNodes = await contracts.ServiceProviderFactoryClient.getServiceProviderList( 'content-node' ) - if (!storageNodes || storageNodes.length === 0) { throw Error('Storage node services not found') } + const antiAbuseAddresses = + await contracts.EthRewardsManagerClient.getAntiAbuseOracleAddresses() + if (!antiAbuseAddresses || antiAbuseAddresses.length === 0) { + throw Error('Anti Abuse node services not found') + } + const antiAbuseOracleNodes = { + endpoints: config.AAO_ENDPOINTS, + addresses: antiAbuseAddresses + } + const minVersion = await contracts.getCurrentVersion('discovery-node') return { minVersion, @@ -112,6 +133,7 @@ const generateServicesConfig = async ( endpoint, delegateOwnerWallet })), + antiAbuseOracleNodes, web3ProviderUrl: config.WEB3_PROVIDER_URL, entityManagerContractAddress: config.ENTITY_MANAGER_CONTRACT_ADDRESS, identityServiceUrl: config.IDENTITY_SERVICE_URL diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index bc888c7ed2b..5ccf0f1a0a1 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -38,6 +38,7 @@ import { SolanaRelay } from './services/Solana/SolanaRelay' import { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' import { SolanaRelayWalletAdapter } from './services/Solana/SolanaRelayWalletAdapter' import { defaultClaimableTokensConfig } from './services/Solana/programs/ClaimableTokens/constants' +import { AntiAbuseOracleSelector } from './services/AntiAbuseOracleSelector/AntiAbuseOracleSelector' /** * The Audius SDK @@ -105,6 +106,8 @@ const initializeServices = (config: SdkConfig) => { const defaultStorage = new Storage({ storageNodeSelector, logger }) + const defaultAntiAbuseOracleSelector = new AntiAbuseOracleSelector({ logger }) + const defaultSolanaRelay = new SolanaRelay({ middleware: [ config.services?.discoveryNodeSelector?.createMiddleware() ?? @@ -128,6 +131,7 @@ const initializeServices = (config: SdkConfig) => { claimableTokensProgram, solanaWalletAdapter: defaultSolanaWalletAdapter, solanaRelay: defaultSolanaRelay, + antiAbuseOracleSelector: defaultAntiAbuseOracleSelector, logger } return { ...defaultServices, ...config.services } diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts new file mode 100644 index 00000000000..f21f87b198c --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts @@ -0,0 +1,57 @@ +import { sampleSize } from 'lodash' +import { mergeConfigWithDefaults } from '../../utils/mergeConfigs' +import type { LoggerService } from '../Logger' +import { defaultAntiAbuseOracleSelectorConfig } from './constants' +import type { + AntiAbuseOracleSelectorService, + AntiAbuseOracle, + AntiAbuseOracleSelectorConfig +} from './types' + +export class AntiAbuseOracleSelector implements AntiAbuseOracleSelectorService { + private services: AntiAbuseOracle[] | null = null + private readonly logger: LoggerService + private readonly endpoints: string[] + private readonly addresses: string[] + constructor(config?: AntiAbuseOracleSelectorConfig) { + const configWithDefaults = mergeConfigWithDefaults( + config, + defaultAntiAbuseOracleSelectorConfig + ) + this.endpoints = configWithDefaults.endpoints + this.addresses = configWithDefaults.addresses + this.logger = configWithDefaults.logger + } + + public async getSelectedService() { + const services = await this.getHealthyRegisteredServices() + return sampleSize(services, 1)[0] ?? null + } + + private async getHealthyRegisteredServices() { + if (this.services === null) { + this.services = [] + for (const endpoint of this.endpoints) { + try { + const response = await fetch(`${endpoint}/health_check`) + if (response.ok) { + const json = await response.json() + const wallet = json.walletPubkey + if (!this.addresses.includes(wallet)) { + throw new Error(`Not registered: ${wallet}`) + } + this.services.push({ + endpoint, + wallet + }) + } else { + throw new Error(`Response failed with status ${response.status}`) + } + } catch (e) { + this.logger.warn(`Anti Abuse Oracle ${endpoint} is unhealthy: ${e}`) + } + } + } + return this.services + } +} diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts new file mode 100644 index 00000000000..68e48f62bf8 --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts @@ -0,0 +1,9 @@ +import { productionConfig } from '../../config' +import { Logger } from '../Logger' +import type { AntiAbuseOracleSelectorConfigInternal } from './types' + +export const defaultAntiAbuseOracleSelectorConfig: AntiAbuseOracleSelectorConfigInternal = + { + ...productionConfig.antiAbuseOracleNodes, + logger: new Logger() + } diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/index.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/index.ts new file mode 100644 index 00000000000..a589b3248fd --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/index.ts @@ -0,0 +1,2 @@ +export { AntiAbuseOracleSelector } from './AntiAbuseOracleSelector' +export * from './types' diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts new file mode 100644 index 00000000000..90eac61095d --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts @@ -0,0 +1,19 @@ +import type { LoggerService } from '../Logger' + +export type AntiAbuseOracle = { + wallet: string + endpoint: string +} + +export type AntiAbuseOracleSelectorService = { + getSelectedService(): Promise +} + +export type AntiAbuseOracleSelectorConfigInternal = { + endpoints: string[] + addresses: string[] + logger: LoggerService +} + +export type AntiAbuseOracleSelectorConfig = + Partial From 10ba3fee568eef0a37552d0bc7500e3d206fbf44 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:31:09 -0800 Subject: [PATCH 12/66] fix create sender script with new reward manager refactor --- .../scripts/createSender.ts | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/discovery-provider/scripts/createSender.ts b/packages/discovery-provider/scripts/createSender.ts index 4ee7a79eeb3..3e22ca4e9d1 100644 --- a/packages/discovery-provider/scripts/createSender.ts +++ b/packages/discovery-provider/scripts/createSender.ts @@ -1,15 +1,12 @@ -import { createSenderInstruction, ethAddress } from '@audius/spl' +import { RewardManagerProgram, ethAddress } from '@audius/spl' import { VersionedTransaction, TransactionMessage, Connection, Keypair, - PublicKey, - Secp256k1Program + PublicKey } from '@solana/web3.js' -import keccak256 from 'keccak256' -import secp256k1 from 'secp256k1' import { program } from 'commander' @@ -75,26 +72,15 @@ const main = async (id: number) => { rewardManagerProgramId ) - const createSender = createSenderInstruction( + const createSender = RewardManagerProgram.createSenderInstruction({ senderEthAddress, - senderEthAddress, - rewardManager, - owner.publicKey, + operatorEthAddress: senderEthAddress, + rewardManagerState: rewardManager, + manager: owner.publicKey, authority, - payer.publicKey, + payer: payer.publicKey, sender, rewardManagerProgramId - ) - - const signResult = secp256k1.ecdsaSign( - Uint8Array.from(keccak256(Buffer.from(createSender.data))), - Buffer.from(senderEthPrivateKey, 'hex') - ) - const secpInstruction = Secp256k1Program.createInstructionWithEthAddress({ - ethAddress: senderEthAddress, - message: Buffer.from(createSender.data), - signature: signResult.signature, - recoveryId: signResult.recid }) const { blockhash, lastValidBlockHeight } = @@ -103,7 +89,7 @@ const main = async (id: number) => { const message = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: blockhash, - instructions: [secpInstruction, createSender] + instructions: [createSender] }).compileToLegacyMessage() const tx = new VersionedTransaction(message) tx.sign([payer, owner]) From 7651aa937509667b981cf5d53d91f56da0a95b5c Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:31:41 -0800 Subject: [PATCH 13/66] include self in discovery node listing on dn --- .../discovery-provider/src/utils/get_all_other_nodes.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/discovery-provider/src/utils/get_all_other_nodes.py b/packages/discovery-provider/src/utils/get_all_other_nodes.py index 0682631e466..43a266af54e 100644 --- a/packages/discovery-provider/src/utils/get_all_other_nodes.py +++ b/packages/discovery-provider/src/utils/get_all_other_nodes.py @@ -91,11 +91,10 @@ async def fetch_results(): for node_info in resp: try: wallet = node_info[3] - if wallet != shared_config["delegate"]["owner_wallet"]: - endpoint = node_info[1] - if is_fqdn(endpoint): - all_other_nodes.append(endpoint) - all_other_wallets.append(wallet) + endpoint = node_info[1] + if is_fqdn(endpoint): + all_other_nodes.append(endpoint) + all_other_wallets.append(wallet) except Exception as e: logger.error( f"get_all_other_nodes.py | ERROR in fetching node info {node_info} generated {e}" From 6578e60b543b496b570ba96aa1ebba23b296a2ff Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:35:55 -0800 Subject: [PATCH 14/66] support new healthcheck format in discoveryNodeSelector --- .../src/sdk/scripts/generateServicesConfig.ts | 13 ++++++++-- .../DiscoveryNodeSelector.ts | 25 +++++++++++++------ .../DiscoveryNodeSelector/healthCheckTypes.ts | 6 +++-- .../services/DiscoveryNodeSelector/types.ts | 8 ++++-- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/libs/src/sdk/scripts/generateServicesConfig.ts b/packages/libs/src/sdk/scripts/generateServicesConfig.ts index a3a1c5c1b0a..79cf5651563 100644 --- a/packages/libs/src/sdk/scripts/generateServicesConfig.ts +++ b/packages/libs/src/sdk/scripts/generateServicesConfig.ts @@ -58,7 +58,13 @@ const envConfigs: Record<'staging' | 'production', EnvironmentConfig> = { const devConfig: ServicesConfig = { minVersion: '0.0.0', - discoveryNodes: ['http://audius-protocol-discovery-provider-1'], + discoveryNodes: [ + { + delegateOwnerWallet: + '0xd09ba371c359f10f22ccda12fd26c598c7921bda3220c9942174562bc6a36fe8', + endpoint: 'http://audius-protocol-discovery-provider-1' + } + ], storageNodes: [ { delegateOwnerWallet: '0x0D38e653eC28bdea5A2296fD5940aaB2D0B8875c', @@ -128,7 +134,10 @@ const generateServicesConfig = async ( const minVersion = await contracts.getCurrentVersion('discovery-node') return { minVersion, - discoveryNodes: discoveryNodes.map((node) => node.endpoint), + discoveryNodes: discoveryNodes.map(({ endpoint, delegateOwnerWallet }) => ({ + endpoint, + delegateOwnerWallet + })), storageNodes: storageNodes.map(({ endpoint, delegateOwnerWallet }) => ({ endpoint, delegateOwnerWallet diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index d7041e37b41..7911b5c5ac0 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -30,7 +30,8 @@ import { DiscoveryNodeSelectorService, DiscoveryNodeSelectorServiceConfig, DiscoveryNodeSelectorServiceConfigInternal, - ServiceSelectionEvents + ServiceSelectionEvents, + DiscoveryNode } from './types' const getPathFromUrl = (url: string) => { @@ -227,7 +228,11 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { const userError = response !== undefined && response.status < 500 if (userError) { - this.logger.warn(`status code ${response.status} below 500, not reselecting`, endpoint, context) + this.logger.warn( + `status code ${response.status} below 500, not reselecting`, + endpoint, + context + ) return response } return await this.reselectAndRetry({ context, endpoint }) @@ -237,7 +242,7 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { onError: async (context: ErrorContext) => { const endpoint = await this.getSelectedEndpoint() const response = context.response - + if (!endpoint) { await this.select(endpoint) } else { @@ -296,7 +301,9 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { // If a whitelist is provided, filter down to it if (this.config.allowlist) { - services = services.filter((s) => this.config.allowlist?.has(s)) + services = services.filter((s) => + this.config.allowlist?.has(s.endpoint) + ) decisionTree.push({ stage: DECISION_TREE_STATE.FILTER_TO_WHITELIST, val: services @@ -305,7 +312,9 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { // if a blacklist is provided, filter out services in the list if (this.config.blocklist) { - services = services.filter((s) => !this.config.blocklist?.has(s)) + services = services.filter( + (s) => !this.config.blocklist?.has(s.endpoint) + ) decisionTree.push({ stage: DECISION_TREE_STATE.FILTER_FROM_BLACKLIST, val: services @@ -319,7 +328,7 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { while (selectedService === null) { // Filter out anything we know is already unhealthy const filteredServices = services.filter( - (s) => !this.unhealthyServices.has(s) + (s) => !this.unhealthyServices.has(s.endpoint) ) decisionTree.push({ stage: DECISION_TREE_STATE.FILTER_OUT_KNOWN_UNHEALTHY, @@ -365,7 +374,9 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { }) // Race this "round" of services to find the quickest healthy node - selectedService = await this.anyHealthyEndpoint(round) + selectedService = await this.anyHealthyEndpoint( + round.map((s) => s.endpoint) + ) attemptedServicesCount += round.length // Retry if none were found diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts index 1aa77c96e42..99ca7d70644 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts @@ -1,5 +1,7 @@ import type { CommsResponse } from '../../api/chats/serverTypes' import type { DeepPartial } from '../../utils/deepPartial' +import type { StorageNode } from '../StorageNodeSelector' +import type { DiscoveryNode } from './types' export type FlaskFullResponse = Partial<{ latest_chain_block: number @@ -55,8 +57,8 @@ export type HealthCheckResponseData = DeepPartial<{ maximum_healthy_block_difference: number meets_min_requirements: boolean network: { - content_nodes: Array<{ endpoint: string; delegateOwnerWallet: string }> - discovery_nodes: string[] + content_nodes: StorageNode[] + discovery_nodes: DiscoveryNode[] } num_users_in_immediate_balance_refresh_queue: number num_users_in_lazy_balance_refresh_queue: number diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts index 961d6ac2b1c..da3c44b0839 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts @@ -34,6 +34,11 @@ export type Backup = BackupHealthData & { endpoint: string } +export type DiscoveryNode = { + endpoint: string + delegateOwnerWallet: string +} + export type DiscoveryNodeSelectorServiceConfigInternal = { /** * Starts the service with a preset selection. Useful for caching/eager loading @@ -73,9 +78,8 @@ export type DiscoveryNodeSelectorServiceConfigInternal = { /** * This should be a list of registered discovery nodes that can be used to * initialize the selection and get the current registered list from. - * @example ['https://discoverynode.audius.co', 'https://disoverynode2.audius.co'] */ - bootstrapServices: string[] + bootstrapServices: DiscoveryNode[] /** * Logger service, defaults to console logging From a7ec40d378fdbbecd749dfcfc9f6beb84091f18c Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:37:05 -0800 Subject: [PATCH 15/66] regen sdk bootstrap config --- packages/libs/src/sdk/config/development.ts | 15 +- packages/libs/src/sdk/config/production.ts | 326 ++++++++++++++++---- packages/libs/src/sdk/config/staging.ts | 40 ++- packages/libs/src/sdk/config/types.ts | 4 +- 4 files changed, 307 insertions(+), 78 deletions(-) diff --git a/packages/libs/src/sdk/config/development.ts b/packages/libs/src/sdk/config/development.ts index 7753d96dd93..1d570cc06d3 100644 --- a/packages/libs/src/sdk/config/development.ts +++ b/packages/libs/src/sdk/config/development.ts @@ -7,7 +7,10 @@ import type { ServicesConfig } from './types' export const servicesConfig: ServicesConfig = { "minVersion": "0.0.0", "discoveryNodes": [ - "http://audius-protocol-discovery-provider-1" + { + "delegateOwnerWallet": "0xd09ba371c359f10f22ccda12fd26c598c7921bda3220c9942174562bc6a36fe8", + "endpoint": "http://audius-protocol-discovery-provider-1" + } ], "storageNodes": [ { @@ -17,5 +20,13 @@ export const servicesConfig: ServicesConfig = { ], "entityManagerContractAddress": "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", "web3ProviderUrl": "http://audius-protocol-poa-ganache-1", - "identityServiceUrl": "http://audius-protocol-identity-service-1" + "identityServiceUrl": "http://audius-protocol-identity-service-1", + "antiAbuseOracleNodes": { + "endpoints": [ + "http://audius-protocol-anti-abuse-oracle-1:8000" + ], + "addresses": [ + "0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3" + ] + } } diff --git a/packages/libs/src/sdk/config/production.ts b/packages/libs/src/sdk/config/production.ts index 44028ff8f9b..a8d8591c5aa 100644 --- a/packages/libs/src/sdk/config/production.ts +++ b/packages/libs/src/sdk/config/production.ts @@ -5,68 +5,248 @@ /* eslint-disable prettier/prettier */ import type { ServicesConfig } from './types' export const servicesConfig: ServicesConfig = { - "minVersion": "0.3.85", + "minVersion": "0.6.0", "discoveryNodes": [ - "https://audius-metadata-1.figment.io", - "https://audius-metadata-2.figment.io", - "https://audius-discovery-1.altego.net", - "https://audius-disco.ams-x01.nl.supercache.org", - "https://dn-jpn.audius.metadata.fyi", - "https://discoveryprovider3.audius.co", - "https://discoveryprovider2.audius.co", - "https://discoveryprovider.audius.co", - "https://audius-metadata-3.figment.io", - "https://audius-metadata-4.figment.io", - "https://dn1.monophonic.digital", - "https://dn-usa.audius.metadata.fyi", - "https://discovery-us-01.audius.openplayer.org", - "https://dn2.monophonic.digital", - "https://audius-discovery-2.altego.net", - "https://dn1.nodeoperator.io", - "https://audius-disco.dfw-x02.us.supercache.org", - "https://audius-discovery-3.altego.net", - "https://dn1.matterlightblooming.xyz", - "https://audius-dp.singapore.creatorseed.com", - "https://discovery.grassfed.network", - "https://audius-discovery-1.cultur3stake.com", - "https://audius-discovery-3.cultur3stake.com", - "https://audius-discovery-4.cultur3stake.com", - "https://audius-discovery-5.cultur3stake.com", - "https://audius-discovery-7.cultur3stake.com", - "https://audius-discovery-8.cultur3stake.com", - "https://audius-discovery-9.cultur3stake.com", - "https://audius-discovery-10.cultur3stake.com", - "https://discovery-au-02.audius.openplayer.org", - "https://disc-lon01.audius.hashbeam.com", - "https://audius-dp.amsterdam.creatorseed.com", - "https://blockdaemon-audius-discovery-01.bdnodes.net", - "https://blockdaemon-audius-discovery-02.bdnodes.net", - "https://blockdaemon-audius-discovery-03.bdnodes.net", - "https://blockdaemon-audius-discovery-04.bdnodes.net", - "https://blockdaemon-audius-discovery-05.bdnodes.net", - "https://blockdaemon-audius-discovery-06.bdnodes.net", - "https://blockdaemon-audius-discovery-07.bdnodes.net", - "https://blockchange-audius-discovery-01.bdnodes.net", - "https://blockchange-audius-discovery-02.bdnodes.net", - "https://blockchange-audius-discovery-03.bdnodes.net", - "https://audius-discovery-11.cultur3stake.com", - "https://audius-discovery-12.cultur3stake.com", - "https://audius-discovery-13.cultur3stake.com", - "https://audius-discovery-14.cultur3stake.com", - "https://audius-discovery-16.cultur3stake.com", - "https://audius-discovery-18.cultur3stake.com", - "https://audius-discovery-17.cultur3stake.com", - "https://audius-discovery-15.cultur3stake.com", - "https://audius-discovery-6.cultur3stake.com", - "https://audius-discovery-2.cultur3stake.com", - "https://blockdaemon-audius-discovery-08.bdnodes.net", - "https://audius-metadata-5.figment.io", - "https://dn1.stuffisup.com", - "https://audius-discovery-1.theblueprint.xyz", - "https://audius-discovery-2.theblueprint.xyz", - "https://audius-discovery-3.theblueprint.xyz", - "https://audius-discovery-4.theblueprint.xyz", - "https://audius.w3coins.io" + { + "endpoint": "https://audius-metadata-1.figment.io", + "delegateOwnerWallet": "0x7db3789e5E2154569e802945ECF2cC92e0994841" + }, + { + "endpoint": "https://audius-metadata-2.figment.io", + "delegateOwnerWallet": "0x4E2C78d0d3303ed459BF8a3CD87f11A6bc936140" + }, + { + "endpoint": "https://audius-discovery-1.altego.net", + "delegateOwnerWallet": "0xE77C7679ED77b175F935755EEb3a421635AF07EC" + }, + { + "endpoint": "https://audius-disco.ams-x01.nl.supercache.org", + "delegateOwnerWallet": "0x4969304A15851812f79796B3C99B6177a0C3Ac32" + }, + { + "endpoint": "https://dn-jpn.audius.metadata.fyi", + "delegateOwnerWallet": "0xE515A7B710e7CBB55F0fB73fc56c15Ad9b36Af9B" + }, + { + "endpoint": "https://discoveryprovider3.audius.co", + "delegateOwnerWallet": "0xF2897993951d53a7E3eb2242D6A14D2028140DC8" + }, + { + "endpoint": "https://discoveryprovider2.audius.co", + "delegateOwnerWallet": "0xc97d40C0B992882646D64814151941A1c520b460" + }, + { + "endpoint": "https://discoveryprovider.audius.co", + "delegateOwnerWallet": "0xf1a1Bd34b2Bc73629aa69E50E3249E89A3c16786" + }, + { + "endpoint": "https://audius-metadata-3.figment.io", + "delegateOwnerWallet": "0xE019F1Ad9803cfC83e11D37Da442c9Dc8D8d82a6" + }, + { + "endpoint": "https://audius-metadata-4.figment.io", + "delegateOwnerWallet": "0xf7441A14A31199744Bf8e7b79405c5446C120D0f" + }, + { + "endpoint": "https://dn1.monophonic.digital", + "delegateOwnerWallet": "0x2CD66a3931C36596efB037b06753476dcE6B4e86" + }, + { + "endpoint": "https://dn-usa.audius.metadata.fyi", + "delegateOwnerWallet": "0x4a3D65647A8Ac41Ef7bdF13D1F171aA97a15ae4b" + }, + { + "endpoint": "https://discovery-us-01.audius.openplayer.org", + "delegateOwnerWallet": "0xaC69a173aC26E2daB8663E210eD87a222Ec3945B" + }, + { + "endpoint": "https://dn2.monophonic.digital", + "delegateOwnerWallet": "0x422541273087beC833c57D3c15B9e17F919bFB1F" + }, + { + "endpoint": "https://audius-discovery-2.altego.net", + "delegateOwnerWallet": "0xA9cB9d043d4841dE83C70556FF0Bd4949C15b5Eb" + }, + { + "endpoint": "https://dn1.nodeoperator.io", + "delegateOwnerWallet": "0x42D35a2f33ba468fA9eB6FFEA4b975F182957556" + }, + { + "endpoint": "https://audius-disco.dfw-x02.us.supercache.org", + "delegateOwnerWallet": "0x4969304A15851812f79796B3C99B6177a0C3Ac32" + }, + { + "endpoint": "https://audius-discovery-3.altego.net", + "delegateOwnerWallet": "0xA9cB9d043d4841dE83C70556FF0Bd4949C15b5Eb" + }, + { + "endpoint": "https://dn1.matterlightblooming.xyz", + "delegateOwnerWallet": "0x67154199E79bEcd2A1f34f89d6AF962CF9863945" + }, + { + "endpoint": "https://audius-dp.singapore.creatorseed.com", + "delegateOwnerWallet": "0x01312a03a859813943Fc2521c31ad500fE86C454" + }, + { + "endpoint": "https://discovery.grassfed.network", + "delegateOwnerWallet": "0xb4c7895739062A54F33998D65eF90afb3689d765" + }, + { + "endpoint": "https://audius-discovery-1.cultur3stake.com", + "delegateOwnerWallet": "0xBFF627Ee7797bB6b06f01AB1709f250Fe88AFc9c" + }, + { + "endpoint": "https://audius-discovery-3.cultur3stake.com", + "delegateOwnerWallet": "0xD3Fe61E45956a3BCE819DD6fC8091E8dBb054cFD" + }, + { + "endpoint": "https://audius-discovery-4.cultur3stake.com", + "delegateOwnerWallet": "0x1b05E1a7E221785BE8D9E7f397962Df9c5539464" + }, + { + "endpoint": "https://audius-discovery-5.cultur3stake.com", + "delegateOwnerWallet": "0x120cd44EE33E17C2F7A6b95dAA0920342f534E21" + }, + { + "endpoint": "https://audius-discovery-7.cultur3stake.com", + "delegateOwnerWallet": "0x6B696B2ae65A885660c3a1DA44b6306509CC2350" + }, + { + "endpoint": "https://audius-discovery-8.cultur3stake.com", + "delegateOwnerWallet": "0x2eDfC1ecD381c991DfcAa6951d7766F4Dbba8CA2" + }, + { + "endpoint": "https://audius-discovery-9.cultur3stake.com", + "delegateOwnerWallet": "0xd8091A289BEf13b5407082Bb66000ccA47e7e34C" + }, + { + "endpoint": "https://audius-discovery-10.cultur3stake.com", + "delegateOwnerWallet": "0x4086DBFb51E451fD1AEeC778FFb884201c944E94" + }, + { + "endpoint": "https://discovery-au-02.audius.openplayer.org", + "delegateOwnerWallet": "0x6CAA3671162bC259094Ea4451d0d16792431C37a" + }, + { + "endpoint": "https://disc-lon01.audius.hashbeam.com", + "delegateOwnerWallet": "0xD3A697f1084e50c19b19a8859E3d746893152c29" + }, + { + "endpoint": "https://audius-dp.amsterdam.creatorseed.com", + "delegateOwnerWallet": "0xd4869005c8aAAB4D53FC5Af24B72617d5D0Ce179" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-01.bdnodes.net", + "delegateOwnerWallet": "0x70256629E87b41105F997878D2Db749a78a5B695" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-02.bdnodes.net", + "delegateOwnerWallet": "0x060e48dd69960829Fb23CB41eB2DFDAc57948FAd" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-03.bdnodes.net", + "delegateOwnerWallet": "0x2416D78b3cc41467c22578dEE7CA90450EB6526e" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-04.bdnodes.net", + "delegateOwnerWallet": "0xbD0548Ce77e69CE22Af591A4155162A08fDDEC3d" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-05.bdnodes.net", + "delegateOwnerWallet": "0xF5EA27b029D5579D344CFa558DDc3B76A39c98d3" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-06.bdnodes.net", + "delegateOwnerWallet": "0x4ACD4eb0F0992cBFf18d5Cb551f3d8790Db01c51" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-07.bdnodes.net", + "delegateOwnerWallet": "0xC7562a5CF872450744C3DC5cDb00e9f105D2EfDc" + }, + { + "endpoint": "https://blockchange-audius-discovery-01.bdnodes.net", + "delegateOwnerWallet": "0xD207D8Eb95aA5b2595cF3EEA14308EB61A36ad21" + }, + { + "endpoint": "https://blockchange-audius-discovery-02.bdnodes.net", + "delegateOwnerWallet": "0xAB30eF276ADC2bE22CE58d75B4F4009173A73676" + }, + { + "endpoint": "https://blockchange-audius-discovery-03.bdnodes.net", + "delegateOwnerWallet": "0x048cFedf907c4C9dDD11ff882380906E78E84BbE" + }, + { + "endpoint": "https://audius-discovery-11.cultur3stake.com", + "delegateOwnerWallet": "0xC6f37525A2EBab1eb02B4c6ba302F402e4c5ad1C" + }, + { + "endpoint": "https://audius-discovery-12.cultur3stake.com", + "delegateOwnerWallet": "0x1354aFF85DfCeF324E8e40d356f53Cd5F0ED4b83" + }, + { + "endpoint": "https://audius-discovery-13.cultur3stake.com", + "delegateOwnerWallet": "0x6f43df165E57598Bd74A2D6ADD18ba4249ECd16B" + }, + { + "endpoint": "https://audius-discovery-14.cultur3stake.com", + "delegateOwnerWallet": "0x0d64915a5F498131474C9A569F0AE0164efB95B5" + }, + { + "endpoint": "https://audius-discovery-16.cultur3stake.com", + "delegateOwnerWallet": "0xD083A0fA8c2d84759f5383EE4655aAb9908E832c" + }, + { + "endpoint": "https://audius-discovery-18.cultur3stake.com", + "delegateOwnerWallet": "0x6C3d9f517a1768dDcDC5e37945e75CAD7A3dF6CC" + }, + { + "endpoint": "https://audius-discovery-17.cultur3stake.com", + "delegateOwnerWallet": "0x06D39081B2811fA7CbADC3D7c4e96889829cdec5" + }, + { + "endpoint": "https://audius-discovery-15.cultur3stake.com", + "delegateOwnerWallet": "0xE34CB31dadA68F046864054E7A500a370F67b973" + }, + { + "endpoint": "https://audius-discovery-6.cultur3stake.com", + "delegateOwnerWallet": "0xf83cA74d5E6AD3F2946754Fa0D1e5cE7670DB764" + }, + { + "endpoint": "https://audius-discovery-2.cultur3stake.com", + "delegateOwnerWallet": "0x7c125128B0917bDE12e6A0eDde8F7675d4ADF408" + }, + { + "endpoint": "https://blockdaemon-audius-discovery-08.bdnodes.net", + "delegateOwnerWallet": "0x8464c88502925a0076c381962F8B70b6EC892861" + }, + { + "endpoint": "https://audius-metadata-5.figment.io", + "delegateOwnerWallet": "0x69cfDc1AB75384f077E4E48cf0d6483C8fB9B8A2" + }, + { + "endpoint": "https://dn1.stuffisup.com", + "delegateOwnerWallet": "0xAA29e93f4008D977078957D8f041AEAeF7e1eeBc" + }, + { + "endpoint": "https://audius-discovery-1.theblueprint.xyz", + "delegateOwnerWallet": "0xEDe07aCa59815fbaa75c4f813dCDD1390D371071" + }, + { + "endpoint": "https://audius-discovery-2.theblueprint.xyz", + "delegateOwnerWallet": "0xCF3f359BfdE7bcAfE4bc058B6DFae51aBe204aB4" + }, + { + "endpoint": "https://audius-discovery-3.theblueprint.xyz", + "delegateOwnerWallet": "0x8449169096550905B903b6803FB3b64285112603" + }, + { + "endpoint": "https://audius-discovery-4.theblueprint.xyz", + "delegateOwnerWallet": "0x16e8DF288BF5DcD507615A715A2a6155F149a865" + }, + { + "endpoint": "https://audius.w3coins.io", + "delegateOwnerWallet": "0x1cBC80a8A4A6fd5793e535B758A71f332DEc1551" + } ], "storageNodes": [ { @@ -90,7 +270,7 @@ export const servicesConfig: ServicesConfig = { "delegateOwnerWallet": "0xBfdE9a7DD3620CB6428463E9A9e9932B4d10fdc5" }, { - "endpoint": "https://creatornode.audius.prod-us-west-2.staked.cloud", + "endpoint": "https://creatornode.audius.prod-eks-ap-northeast-1.staked.cloud", "delegateOwnerWallet": "0x675086B880260D217963cF14F503272AEb44b2E9" }, { @@ -110,15 +290,15 @@ export const servicesConfig: ServicesConfig = { "delegateOwnerWallet": "0x10fF8197f2e94eF880d940D2414E0A14983c3bFE" }, { - "endpoint": "https://creatornode.audius1.prod-us-west-2.staked.cloud", + "endpoint": "https://creatornode.audius1.prod-eks-ap-northeast-1.staked.cloud", "delegateOwnerWallet": "0xC23Ee959E0B22a9B0F5dF18D7e7875cA4B6c4236" }, { - "endpoint": "https://creatornode.audius2.prod-us-west-2.staked.cloud", + "endpoint": "https://creatornode.audius2.prod-eks-ap-northeast-1.staked.cloud", "delegateOwnerWallet": "0x51a5575dc04c1f5f2e39390d090aaf78554F5f7B" }, { - "endpoint": "https://creatornode.audius3.prod-us-west-2.staked.cloud", + "endpoint": "https://creatornode.audius3.prod-eks-ap-northeast-1.staked.cloud", "delegateOwnerWallet": "0xe0b56BAe2276E016d3DB314Dd7374e596B0457ac" }, { @@ -340,8 +520,24 @@ export const servicesConfig: ServicesConfig = { { "endpoint": "https://creatornode.audius8.prod-eks-ap-northeast-1.staked.cloud", "delegateOwnerWallet": "0xc69F344FCDbc9D747559c968562f682ABfBa442C" + }, + { + "endpoint": "https://cn1.stuffisup.com", + "delegateOwnerWallet": "0x0D16f8bBfFF114B1a525Bf8b8d98ED177FA74AD3" } ], + "antiAbuseOracleNodes": { + "endpoints": [ + "https://antiabuseoracle.audius.co", + "https://audius-oracle.creatorseed.com", + "https://oracle.audius.endl.net" + ], + "addresses": [ + "0x9811BA3eAB1F2Cd9A2dFeDB19e8c2a69729DC8b6", + "0xe60d50356cd891f56B744165fcc1D8B352201A76", + "0x7A03cFAE79266683D9706731D6E187868E939c9C" + ] + }, "web3ProviderUrl": "https://poa-gateway.audius.co", "entityManagerContractAddress": "0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64", "identityServiceUrl": "https://identityservice.audius.co" diff --git a/packages/libs/src/sdk/config/staging.ts b/packages/libs/src/sdk/config/staging.ts index 7d20b38d37e..81aab511a3b 100644 --- a/packages/libs/src/sdk/config/staging.ts +++ b/packages/libs/src/sdk/config/staging.ts @@ -5,18 +5,26 @@ /* eslint-disable prettier/prettier */ import type { ServicesConfig } from './types' export const servicesConfig: ServicesConfig = { - "minVersion": "0.3.83", + "minVersion": "0.4.4", "discoveryNodes": [ - "https://discoveryprovider2.staging.audius.co", - "https://discoveryprovider3.staging.audius.co", - "https://discoveryprovider.staging.audius.co", - "https://discoveryprovider5.staging.audius.co" - ], - "storageNodes": [ { - "endpoint": "https://usermetadata.staging.audius.co", - "delegateOwnerWallet": "0x671ddce7B4E676C9467F87e4031a917b5D6f75F0" + "endpoint": "https://discoveryprovider2.staging.audius.co", + "delegateOwnerWallet": "0x5E98cBEEAA2aCEDEc0833AC3D1634E2A7aE0f3c2" + }, + { + "endpoint": "https://discoveryprovider3.staging.audius.co", + "delegateOwnerWallet": "0xf7C96916bd37Ad76D4EEDd6536B81c29706C8056" + }, + { + "endpoint": "https://discoveryprovider.staging.audius.co", + "delegateOwnerWallet": "0x8fcFA10Bd3808570987dbb5B1EF4AB74400FbfDA" }, + { + "endpoint": "https://discoveryprovider5.staging.audius.co", + "delegateOwnerWallet": "0x8311f59B72522e728231dC60226359A51878F9A1" + } + ], + "storageNodes": [ { "endpoint": "https://creatornode5.staging.audius.co", "delegateOwnerWallet": "0xDC2BDF1F23381CA2eC9e9c70D4FD96CD8645D090" @@ -44,8 +52,22 @@ export const servicesConfig: ServicesConfig = { { "endpoint": "https://creatornode11.staging.audius.co", "delegateOwnerWallet": "0x4c88d2c0f4c4586b41621aD6e98882ae904B98f6" + }, + { + "endpoint": "https://creatornode12.staging.audius.co", + "delegateOwnerWallet": "0x6b52969934076318863243fb92E9C4b3A08267b5" } ], + "antiAbuseOracleNodes": { + "endpoints": [ + "https://antiabuseoracle.staging.audius.co" + ], + "addresses": [ + "0x00b6462e955dA5841b6D9e1E2529B830F00f31Bf", + "0x57B57efFA54ba37DBF8A06B9c42E7611e84BDe6F", + "0xF617bbc0913bAE0a13f6D4A19eCDE5Aa07B0fF0A" + ] + }, "web3ProviderUrl": "https://poa-gateway.staging.audius.co", "entityManagerContractAddress": "0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64", "identityServiceUrl": "https://identityservice.staging.audius.co" diff --git a/packages/libs/src/sdk/config/types.ts b/packages/libs/src/sdk/config/types.ts index be837a3d8dd..1ce6a8beaa0 100644 --- a/packages/libs/src/sdk/config/types.ts +++ b/packages/libs/src/sdk/config/types.ts @@ -1,8 +1,8 @@ -import type { StorageNode } from '../services' +import type { DiscoveryNode, StorageNode } from '../services' export type ServicesConfig = { minVersion: string - discoveryNodes: string[] + discoveryNodes: DiscoveryNode[] storageNodes: StorageNode[] antiAbuseOracleNodes: { endpoints: string[] From 84cf38e0a05db050fb934122f905fb4b7635b0c3 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:38:53 -0800 Subject: [PATCH 16/66] add antiAbuseOracleSelector to servicescontainer type --- packages/libs/src/sdk/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index 91fa5aaf567..4ff4de94292 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -8,6 +8,7 @@ import type { StorageService } from './services/Storage' import type { StorageNodeSelectorService } from './services/StorageNodeSelector' import type { SolanaRelayService, SolanaWalletAdapter } from './services/Solana' import type { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' +import type { AntiAbuseOracleSelectorService } from './services/AntiAbuseOracleSelector/types' export type ServicesContainer = { /** @@ -54,6 +55,11 @@ export type ServicesContainer = { * Claimable Tokens Program client for Solana */ claimableTokensProgram: ClaimableTokens + + /** + * Service used to choose a healthy Anti Abuse Oracle + */ + antiAbuseOracleSelector: AntiAbuseOracleSelectorService } const DevAppSchema = z.object({ From ee4d7a89f7d483fddf0b013fe8afab5b6d341c32 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:39:54 -0800 Subject: [PATCH 17/66] fix solana relay wallet adapter so that it connects or throws --- .../src/sdk/services/Solana/SolanaRelayWalletAdapter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts index da4ed806563..96894f4fb63 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts @@ -28,9 +28,9 @@ export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { private _publicKey: PublicKey | null = null private _connecting = false - private _connected = true + private _connected = false - constructor(private solanaRelay: SolanaRelay) {} + constructor(private readonly solanaRelay: SolanaRelay) {} public get publicKey() { return this._publicKey @@ -54,6 +54,11 @@ export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { public async connect() { this._connecting = true this._publicKey = await this.solanaRelay.getFeePayer() + if (!this._publicKey) { + throw new Error( + 'Failed to connect SolanaRelayWalletAdapter: Failed to get fee payer.' + ) + } this._connecting = false this._connected = true } From 0dba993a7682c4cf84a0c4147996e195c9ce6b9d Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:40:35 -0800 Subject: [PATCH 18/66] audius/spl claimable tokens: add explicit public and don't redeclare enum --- .../ClaimableTokensProgram.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/spl/src/claimable-tokens/ClaimableTokensProgram.ts b/packages/spl/src/claimable-tokens/ClaimableTokensProgram.ts index eca5741f150..0d066897bee 100644 --- a/packages/spl/src/claimable-tokens/ClaimableTokensProgram.ts +++ b/packages/spl/src/claimable-tokens/ClaimableTokensProgram.ts @@ -12,6 +12,7 @@ import bs58 from 'bs58' import { ethAddress } from '../layout-utils' +import { ClaimableTokensInstruction } from './constants' import { CreateClaimableTokensAccountParams, CreateClaimableTokensAccountInstructionData, @@ -28,10 +29,7 @@ const TRANSFER_NONCE_PREFIX = 'N_' const TRANSFER_NONCE_PREFIX_BYTES = new TextEncoder().encode( TRANSFER_NONCE_PREFIX ) -enum ClaimableTokensInstruction { - Create = 0, - Transfer = 1 -} + /** @see {@link https://github.com/solana-labs/solana-web3.js/blob/974193946d5e6fade11b96d141f21ebe8f3ff5e2/packages/library-legacy/src/programs/secp256k1.ts#L47C11-L47C11 SECP256K1_INSTRUCTION_LAYOUT} */ const SECP256K1_INSTRUCTION_MESSAGE_DATA_START = 97 @@ -49,11 +47,11 @@ const SECP256K1_INSTRUCTION_MESSAGE_DATA_START = 97 * A user can have multiple user banks, one for each token mint. */ export class ClaimableTokensProgram { - static programId = new PublicKey( + public static readonly programId = new PublicKey( 'Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ' ) - static layouts = { + public static readonly layouts = { createAccountInstructionData: struct([ u8('instruction'), @@ -73,7 +71,7 @@ export class ClaimableTokensProgram { nonceAccountData: struct([u8('version'), u64('nonce')]) } - static createAccountInstruction({ + public static createAccountInstruction({ ethAddress, payer, mint, @@ -101,7 +99,7 @@ export class ClaimableTokensProgram { return new TransactionInstruction({ keys, programId, data }) } - static decodeCreateAccountInstruction({ + public static decodeCreateAccountInstruction({ programId, keys: [ payer, @@ -131,7 +129,7 @@ export class ClaimableTokensProgram { } } - static createTransferInstruction({ + public static createTransferInstruction({ payer, sourceEthAddress, sourceUserBank, @@ -169,7 +167,7 @@ export class ClaimableTokensProgram { return new TransactionInstruction({ programId, keys, data }) } - static decodeTransferInstruction({ + public static decodeTransferInstruction({ programId, keys: [ payer, @@ -203,7 +201,7 @@ export class ClaimableTokensProgram { } } - static decodeInstruction( + public static decodeInstruction( instruction: TransactionInstruction ): DecodedClaimableTokenInstruction { switch (instruction.data[0]) { @@ -218,19 +216,19 @@ export class ClaimableTokensProgram { } } - static isCreateAccountInstruction( + public static isCreateAccountInstruction( decoded: DecodedClaimableTokenInstruction ): decoded is DecodedCreateClaimableTokensAccountInstruction { return decoded.data.instruction === ClaimableTokensInstruction.Create } - static isTransferInstruction( + public static isTransferInstruction( decoded: DecodedClaimableTokenInstruction ): decoded is DecodedTransferClaimableTokensInstruction { return decoded.data.instruction === ClaimableTokensInstruction.Transfer } - static createSignedTransferInstructionData({ + public static createSignedTransferInstructionData({ destination, amount, nonce @@ -249,7 +247,7 @@ export class ClaimableTokensProgram { return data } - static decodeSignedTransferInstructionData( + public static decodeSignedTransferInstructionData( instruction: TransactionInstruction ) { return ClaimableTokensProgram.layouts.signedTransferInstructionData.decode( @@ -259,7 +257,7 @@ export class ClaimableTokensProgram { ) } - static deriveNonce({ + public static deriveNonce({ ethAddress: wallet, programId, authority @@ -278,7 +276,7 @@ export class ClaimableTokensProgram { )[0] } - static async deriveUserBank({ + public static async deriveUserBank({ ethAddress: wallet, claimableTokensPDA, tokenProgramId = TOKEN_PROGRAM_ID @@ -298,7 +296,7 @@ export class ClaimableTokensProgram { ) } - static deriveAuthority = ({ + public static deriveAuthority = ({ programId, mint }: { From 71d52cc7a031cc92daa6195e559e0866bb912786 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:45:20 -0800 Subject: [PATCH 19/66] add RewardManager program service --- packages/libs/src/sdk/sdk.ts | 16 +- .../libs/src/sdk/services/Solana/index.ts | 3 + .../programs/RewardManager/RewardManager.ts | 242 ++++++++++++++++++ .../programs/RewardManager/constants.ts | 10 + .../Solana/programs/RewardManager/types.ts | 71 +++++ packages/libs/src/sdk/types.ts | 13 +- 6 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts create mode 100644 packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts create mode 100644 packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index 5ccf0f1a0a1..1b357dd2699 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -28,7 +28,8 @@ import { Auth, Storage, EntityManager, - AppAuth + AppAuth, + RewardManager } from './services' import { defaultEntityManagerConfig } from './services/EntityManager/constants' import { Logger } from './services/Logger' @@ -38,6 +39,7 @@ import { SolanaRelay } from './services/Solana/SolanaRelay' import { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' import { SolanaRelayWalletAdapter } from './services/Solana/SolanaRelayWalletAdapter' import { defaultClaimableTokensConfig } from './services/Solana/programs/ClaimableTokens/constants' +import { defaultRewardManagerConfig } from './services/Solana/programs/RewardManager/constants' import { AntiAbuseOracleSelector } from './services/AntiAbuseOracleSelector/AntiAbuseOracleSelector' /** @@ -114,21 +116,29 @@ const initializeServices = (config: SdkConfig) => { defaultDiscoveryNodeSelector.createMiddleware() ] }) + const defaultSolanaWalletAdapter = new SolanaRelayWalletAdapter( config.services?.solanaRelay ?? defaultSolanaRelay ) - const claimableTokensProgram = new ClaimableTokens( + + const defaultClaimableTokensProgram = new ClaimableTokens( defaultClaimableTokensConfig, config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter ) + const defaultRewardManagerProgram = new RewardManager( + defaultRewardManagerConfig, + config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter + ) + const defaultServices: ServicesContainer = { storageNodeSelector, discoveryNodeSelector: defaultDiscoveryNodeSelector, entityManager: defaultEntityManager, storage: defaultStorage, auth: defaultAuthService, - claimableTokensProgram, + claimableTokensProgram: defaultClaimableTokensProgram, + rewardManagerProgram: defaultRewardManagerProgram, solanaWalletAdapter: defaultSolanaWalletAdapter, solanaRelay: defaultSolanaRelay, antiAbuseOracleSelector: defaultAntiAbuseOracleSelector, diff --git a/packages/libs/src/sdk/services/Solana/index.ts b/packages/libs/src/sdk/services/Solana/index.ts index 502ec875b4f..59583f979c3 100644 --- a/packages/libs/src/sdk/services/Solana/index.ts +++ b/packages/libs/src/sdk/services/Solana/index.ts @@ -1,2 +1,5 @@ export * from './SolanaRelay' +export * from './SolanaRelayWalletAdapter' +export * from './programs/ClaimableTokens/ClaimableTokens' +export * from './programs/RewardManager/RewardManager' export * from './types' diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts new file mode 100644 index 00000000000..433984a5d82 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts @@ -0,0 +1,242 @@ +import { RewardManagerProgram, ethAddress } from '@audius/spl' +import type { SolanaWalletAdapter } from '../../types' +import { SolanaProgram } from '../SolanaProgram' +import { + CreateEvaluateAttestationsInstructionRequest, + CreateEvaluateAttestationsInstructionSchema, + CreateSubmitAttestationInstructionSchema, + type CreateSubmitAttestationRequest, + type RewardManagerConfig, + CreateSenderInstructionRequest, + CreateSenderInstructionSchema, + CreateSubmitAttestationSecpInstructionRequest, + CreateSubmitAttestationSecpInstructionSchema, + GetSubmittedAttestationsRequest, + GetSubmittedAttestationsSchema +} from './types' +import { Secp256k1Program, type PublicKey } from '@solana/web3.js' +import { parseParams } from '../../../../utils/parseParams' +import type { RewardManagerStateData } from '@audius/spl/dist/types/reward-manager/types' +import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' +import { defaultRewardManagerConfig } from './constants' + +export class RewardManager extends SolanaProgram { + private readonly programId: PublicKey + private readonly rewardManagerStateAccount: PublicKey + private readonly authority: PublicKey + private rewardManagerState: RewardManagerStateData | null = null + + constructor( + config: RewardManagerConfig, + solanaWalletAdapter: SolanaWalletAdapter + ) { + const configWithDefaults = mergeConfigWithDefaults( + config, + defaultRewardManagerConfig + ) + super(configWithDefaults, solanaWalletAdapter) + this.programId = configWithDefaults.programId + this.rewardManagerStateAccount = configWithDefaults.rewardManagerState + this.authority = RewardManagerProgram.deriveAuthority({ + programId: configWithDefaults.programId, + rewardManagerState: configWithDefaults.rewardManagerState + }) + } + + public async createSenderInstruction(params: CreateSenderInstructionRequest) { + const args = await parseParams( + 'createSenderInstruction', + CreateSenderInstructionSchema + )(params) + const { + manager, + sender: senderEthAddress, + operator: operatorEthAddress, + feePayer: feePayerOverride + } = args + const feePayer = feePayerOverride ?? (await this.getFeePayer()) + const sender = RewardManagerProgram.deriveSender({ + ethAddress: senderEthAddress, + programId: this.programId, + authority: this.authority + }) + + return RewardManagerProgram.createSenderInstruction({ + operatorEthAddress, + senderEthAddress, + rewardManagerState: this.rewardManagerStateAccount, + manager, + authority: this.authority, + payer: feePayer, + sender, + rewardManagerProgramId: this.programId + }) + } + + public async createSubmitAttestationInstruction( + params: CreateSubmitAttestationRequest + ) { + const args = await parseParams( + 'createSubmitAttestationInstruction', + CreateSubmitAttestationInstructionSchema + )(params) + const { + challengeId, + specifier, + senderEthAddress, + feePayer: feePayerOverride + } = args + const disbursementId = this.makeDisbursementId(challengeId, specifier) + const feePayer = feePayerOverride ?? (await this.getFeePayer()) + const sender = RewardManagerProgram.deriveSender({ + ethAddress: senderEthAddress, + programId: this.programId, + authority: this.authority + }) + const attestations = RewardManagerProgram.deriveAttestations({ + disbursementId, + programId: this.programId, + authority: this.authority + }) + return RewardManagerProgram.createSubmitAttestationInstruction({ + disbursementId, + attestations, + rewardManagerState: this.rewardManagerStateAccount, + authority: this.authority, + payer: feePayer, + sender, + rewardManagerProgramId: this.programId + }) + } + + public async createSubmitAttestationSecpInstruction( + params: CreateSubmitAttestationSecpInstructionRequest + ) { + const args = await parseParams( + 'createSubmitAttestationSecpInstruction', + CreateSubmitAttestationSecpInstructionSchema + )(params) + const { + recipientEthAddress, + challengeId, + specifier, + amount, + senderEthAddress, + senderSignature, + instructionIndex, + antiAbuseOracleEthAddress + } = args + + const disbursementId = this.makeDisbursementId(challengeId, specifier) + const { signature, recoveryId } = + RewardManagerProgram.encodeSignature(senderSignature) + + const data = RewardManagerProgram.encodeAttestation({ + disbursementId, + recipientEthAddress, + amount, + antiAbuseOracleEthAddress + }) + + return Secp256k1Program.createInstructionWithEthAddress({ + ethAddress: senderEthAddress, + message: data, + signature, + recoveryId, + instructionIndex + }) + } + + public async createEvaluateAttestationsInstruction( + params: CreateEvaluateAttestationsInstructionRequest + ) { + const args = await parseParams( + 'createEvaluateAttestationsInstruction', + CreateEvaluateAttestationsInstructionSchema + )(params) + const { + challengeId, + specifier, + recipientEthAddress, + destinationUserBank, + antiAbuseOracleEthAddress, + amount, + feePayer: feePayerOverride + } = args + const disbursementId = this.makeDisbursementId(challengeId, specifier) + const feePayer = feePayerOverride ?? (await this.getFeePayer()) + const state = await this.getRewardManagerState() + const disbursementAccount = RewardManagerProgram.deriveDisbursement({ + disbursementId, + programId: this.programId, + authority: this.authority + }) + const attestations = RewardManagerProgram.deriveAttestations({ + disbursementId, + programId: this.programId, + authority: this.authority + }) + const antiAbuseOracle = RewardManagerProgram.deriveSender({ + ethAddress: antiAbuseOracleEthAddress, + programId: this.programId, + authority: this.authority + }) + return RewardManagerProgram.createEvaluateAttestationsInstruction({ + disbursementId, + recipientEthAddress, + amount, + attestations, + rewardManagerState: this.rewardManagerStateAccount, + authority: this.authority, + rewardManagerTokenSource: state.tokenAccount, + destinationUserBank, + disbursementAccount, + antiAbuseOracle, + payer: feePayer, + rewardManagerProgramId: this.programId + }) + } + + public async getSubmittedAttestations( + params: GetSubmittedAttestationsRequest + ) { + const args = await parseParams( + 'getSubmittedAttestations', + GetSubmittedAttestationsSchema + )(params) + const { challengeId, specifier } = args + const disbursementId = this.makeDisbursementId(challengeId, specifier) + const attestationsAccount = RewardManagerProgram.deriveAttestations({ + disbursementId, + programId: this.programId, + authority: this.authority + }) + const accountInfo = await this.connection.getAccountInfo( + attestationsAccount + ) + if (!accountInfo) { + return null + } + return RewardManagerProgram.decodeAttestationsAccountData(accountInfo.data) + } + + private makeDisbursementId(challengeId: string, specifier: string) { + return `${challengeId}:${specifier}` + } + + public async getRewardManagerState() { + if (!this.rewardManagerState) { + const state = await this.connection.getAccountInfo( + this.rewardManagerStateAccount + ) + if (state) { + this.rewardManagerState = RewardManagerProgram.decodeRewardManagerState( + state.data + ) + } else { + throw new Error('Failed to get reward manager account state.') + } + } + return this.rewardManagerState + } +} diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts new file mode 100644 index 00000000000..62480716942 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts @@ -0,0 +1,10 @@ +import { PublicKey } from '@solana/web3.js' +import type { RewardManagerConfigInternal } from './types' + +export const defaultRewardManagerConfig: RewardManagerConfigInternal = { + programId: new PublicKey('Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ'), + rpcEndpoint: 'https://api.mainnet-beta.solana.com', + rewardManagerState: new PublicKey( + '3V9opXNpHmPPymKeq7CYD8wWMH8wzFXmqEkNdzfsZhYq' + ) +} diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts new file mode 100644 index 00000000000..157a8982013 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts @@ -0,0 +1,71 @@ +import type { PublicKey } from '@solana/web3.js' +import type { SolanaProgramConfigInternal } from '../types' +import { z } from 'zod' +import { PublicKeySchema } from '../../types' + +export type RewardManagerConfigInternal = { + programId: PublicKey + rewardManagerState: PublicKey +} & SolanaProgramConfigInternal + +export type RewardManagerConfig = Partial + +export const CreateSenderInstructionSchema = z.object({ + manager: PublicKeySchema, + sender: z.string(), + operator: z.string(), + feePayer: PublicKeySchema.optional() +}) + +export type CreateSenderInstructionRequest = z.input< + typeof CreateSenderInstructionSchema +> + +export const CreateSubmitAttestationInstructionSchema = z.object({ + challengeId: z.string(), + specifier: z.string(), + senderEthAddress: z.string(), + feePayer: PublicKeySchema.optional() +}) + +export type CreateSubmitAttestationRequest = z.input< + typeof CreateSubmitAttestationInstructionSchema +> + +export const CreateSubmitAttestationSecpInstructionSchema = z.object({ + challengeId: z.string(), + specifier: z.string(), + recipientEthAddress: z.string(), + senderEthAddress: z.string(), + amount: z.bigint(), + antiAbuseOracleEthAddress: z.string().optional(), + senderSignature: z.string(), + instructionIndex: z.number().optional() +}) + +export type CreateSubmitAttestationSecpInstructionRequest = z.input< + typeof CreateSubmitAttestationSecpInstructionSchema +> + +export const CreateEvaluateAttestationsInstructionSchema = z.object({ + challengeId: z.string(), + specifier: z.string(), + recipientEthAddress: z.string(), + destinationUserBank: PublicKeySchema, + antiAbuseOracleEthAddress: z.string(), + amount: z.bigint(), + feePayer: PublicKeySchema.optional() +}) + +export type CreateEvaluateAttestationsInstructionRequest = z.input< + typeof CreateEvaluateAttestationsInstructionSchema +> + +export const GetSubmittedAttestationsSchema = z.object({ + challengeId: z.string(), + specifier: z.string() +}) + +export type GetSubmittedAttestationsRequest = z.input< + typeof GetSubmittedAttestationsSchema +> diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index 4ff4de94292..b00899ba92a 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -6,8 +6,12 @@ import type { EntityManagerService } from './services/EntityManager' import type { LoggerService } from './services/Logger' import type { StorageService } from './services/Storage' import type { StorageNodeSelectorService } from './services/StorageNodeSelector' -import type { SolanaRelayService, SolanaWalletAdapter } from './services/Solana' -import type { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' +import type { + RewardManager, + ClaimableTokens, + SolanaRelayService, + SolanaWalletAdapter +} from './services/Solana' import type { AntiAbuseOracleSelectorService } from './services/AntiAbuseOracleSelector/types' export type ServicesContainer = { @@ -56,6 +60,11 @@ export type ServicesContainer = { */ claimableTokensProgram: ClaimableTokens + /** + * Reward Manager Program client for Solana + */ + rewardManagerProgram: RewardManager + /** * Service used to choose a healthy Anti Abuse Oracle */ From c1368412a30c2b84b732805c657c1ab4f6de1cf5 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:47:35 -0800 Subject: [PATCH 20/66] DiscoveryNodes type for dn selector private member missed changing this type in a different commit --- .../sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index 7911b5c5ac0..ff9a7b6ec3b 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -51,7 +51,7 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { /** * List of services to select from */ - private services: string[] + private services: DiscoveryNode[] /** * Currently selected discovery node From 6182f893fba765d8c9ad6062a6dfeb383cf8e0ee Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:48:21 -0800 Subject: [PATCH 21/66] forgot to export antiabuseselector from services index --- packages/libs/src/sdk/services/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/libs/src/sdk/services/index.ts b/packages/libs/src/sdk/services/index.ts index 1853e8c6b47..268b19ad66c 100644 --- a/packages/libs/src/sdk/services/index.ts +++ b/packages/libs/src/sdk/services/index.ts @@ -5,3 +5,4 @@ export * from './Storage' export * from './Auth' export * from './Logger' export * from './Solana' +export * from './AntiAbuseOracleSelector' From 47b2af042b959abc05b5a134f9d1794f971794c2 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:50:34 -0800 Subject: [PATCH 22/66] add helper method to refresh service list --- .../DiscoveryNodeSelector.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index ff9a7b6ec3b..88ec1f8c493 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -267,7 +267,12 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { /** * Gets the list of services */ - public getServices() { + public async getServices() { + const selected = await this.getSelectedEndpoint() + if (selected) { + // refresh the list + await this.refreshServiceList(selected) + } return this.services } @@ -460,17 +465,7 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { // Cancel any existing requests from other promises abortController.abort() // Refresh service list with the healthy list from DN - if ( - data?.network?.discovery_nodes && - data.network.discovery_nodes.length > 0 - ) { - this.services = data.network.discovery_nodes - } else { - this.logger.warn( - "Couldn't load new service list from healthy service", - endpoint - ) - } + await this.refreshServiceList(endpoint, data?.network?.discovery_nodes) return endpoint } }) @@ -483,6 +478,25 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { } } + private async refreshServiceList(endpoint: string, nodes?: DiscoveryNode[]) { + if (!nodes) { + const { data } = await getDiscoveryNodeHealthCheck({ + endpoint, + healthCheckThresholds: this.config.healthCheckThresholds + }) + nodes = data?.network?.discovery_nodes + } + if (nodes && nodes.length > 0) { + this.logger.debug(`Refreshed service list with ${nodes.length} nodes.`) + this.services = nodes + } else { + this.logger.warn( + "Couldn't load new service list from healthy service", + endpoint + ) + } + } + /** * Checks the given endpoint's health check and reselects if necessary. * @param endpoint the endpoint to health_check From 81d0178e1fc82afd45358b0c9791053051d9ccf8 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:50:58 -0800 Subject: [PATCH 23/66] fix public key schema to transform strings to pubkeys --- packages/libs/src/sdk/services/Solana/types.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/libs/src/sdk/services/Solana/types.ts b/packages/libs/src/sdk/services/Solana/types.ts index 6c073892c0b..068d3c675de 100644 --- a/packages/libs/src/sdk/services/Solana/types.ts +++ b/packages/libs/src/sdk/services/Solana/types.ts @@ -27,14 +27,14 @@ export const MintSchema = z.enum(['wAUDIO', 'USDC']).default('wAUDIO') export type Mint = z.infer -export const PublicKeySchema = z.custom((data) => { - try { - new PublicKey(data as PublicKey) - return true - } catch { - return false - } -}) +export const PublicKeySchema = z.union([ + z.string().transform((data) => { + return new PublicKey(data) + }), + z.custom((data) => { + return data instanceof PublicKey + }) +]) export const RelaySchema = z .object({ From 1157a6bdf1b86e3bee5e9b737ce03c0f0eb713da Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:52:40 -0800 Subject: [PATCH 24/66] add helper to confirm all transactions to all solana program services --- .../services/Solana/programs/SolanaProgram.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts b/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts index a3b396efeb4..01bcb09c5fc 100644 --- a/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts +++ b/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts @@ -1,4 +1,5 @@ import { + Commitment, Connection, PublicKey, TransactionMessage, @@ -44,6 +45,38 @@ export class SolanaProgram { ) } + /** + * Confirms all the transactions provided + */ + public async confirmAllTransactions( + signatures: string[], + commitment: Commitment = 'confirmed' + ) { + const { blockhash, lastValidBlockHeight } = + await this.connection.getLatestBlockhash() + const results = await Promise.all( + signatures.map(async (signature) => { + const res = await this.connection.confirmTransaction( + { + signature, + blockhash, + lastValidBlockHeight + }, + commitment + ) + return { signature, err: res.value.err } + }) + ) + const errors = results.filter((r) => !!r.err) + if (errors.length > 0) { + throw new Error( + `Failed to confirm transactions: ${errors + .map((e) => `${e.signature}: ${e.err}`) + .join(', ')}` + ) + } + } + /** * Convenience helper to construct v0 transactions. * From 66d54c1b77a98804ea30733a1a758adc0be81ad2 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:54:00 -0800 Subject: [PATCH 25/66] first impl of getUniquelyOwnedEndpoints (wet) --- .../DiscoveryNodeSelector.ts | 137 ++++++++++++++++++ .../services/DiscoveryNodeSelector/types.ts | 5 + 2 files changed, 142 insertions(+) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index 88ec1f8c493..d3f54f9ad98 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -264,6 +264,143 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { return await this.select(null) } + public async getUniquelyOwnedEndpoints( + nodeCount: number, + excludeOwners: string[] = [] + ) { + const decisionTree: Decision[] = [] + + // Get all services + let services = await this.getServices() + decisionTree.push({ + stage: DECISION_TREE_STATE.GET_ALL_SERVICES, + val: services + }) + + if (excludeOwners) { + services = services.filter( + (s) => !excludeOwners.includes(s.delegateOwnerWallet) + ) + decisionTree.push({ + stage: DECISION_TREE_STATE.EXCLUDE_OWNERS, + val: services + }) + } + + // If a whitelist is provided, filter down to it + if (this.config.allowlist) { + services = services.filter((s) => this.config.allowlist?.has(s.endpoint)) + decisionTree.push({ + stage: DECISION_TREE_STATE.FILTER_TO_WHITELIST, + val: services + }) + } + + // if a blacklist is provided, filter out services in the list + if (this.config.blocklist) { + services = services.filter((s) => !this.config.blocklist?.has(s.endpoint)) + decisionTree.push({ + stage: DECISION_TREE_STATE.FILTER_FROM_BLACKLIST, + val: services + }) + } + + // Filter out anything we know is already unhealthy + const filteredServices = services.filter( + (s) => !this.unhealthyServices.has(s.endpoint) + ) + decisionTree.push({ + stage: DECISION_TREE_STATE.FILTER_OUT_KNOWN_UNHEALTHY, + val: filteredServices + }) + + // Group services by owner + const endpointsByOwner = filteredServices.reduce>( + (acc, cur) => { + if (cur.delegateOwnerWallet in acc) { + acc[cur.delegateOwnerWallet]!.push(cur.endpoint) + } else { + acc[cur.delegateOwnerWallet] = [cur.endpoint] + } + return acc + }, + {} + ) + + // Find nodeCount healthy nodes from unique owners + const ownersToTry = new Set(Object.keys(endpointsByOwner)) + if (ownersToTry.size < nodeCount) { + this.logger.error('Not enough unique owners to choose from', { + decisionTree + }) + throw new Error('Not enough unique owners to choose from') + } + + const selectedNodes: string[] = [] + while (ownersToTry.size > 0 && selectedNodes.length < nodeCount) { + // Get an unattempted owner + const [sampledOwner] = sampleSize(Object.keys(endpointsByOwner), 1) + ownersToTry.delete(sampledOwner!) // always defined + + // Get the owner's services + const servicesToTry = new Set(endpointsByOwner[sampledOwner!]) + + // Find a healthy node from this owner + let healthyService: string | null = null + let attemptedServicesCount = 0 + while (healthyService === null && servicesToTry.size > 0) { + // Get a round to select from + const round = sampleSize( + [...servicesToTry], + this.config.maxConcurrentRequests + ) + round.forEach((s) => servicesToTry.delete(s)) + decisionTree.push({ + stage: DECISION_TREE_STATE.GET_SELECTION_ROUND, + val: round + }) + attemptedServicesCount += round.length + healthyService = await this.anyHealthyEndpoint(round) + if (!healthyService) { + decisionTree.push({ + stage: DECISION_TREE_STATE.ROUND_FAILED_RETRY + }) + this.logger.debug( + 'No healthy services found. Attempting another round...', + { + attemptedServicesCount + } + ) + } + } + if (healthyService === null) { + // Don't try backups, just go to the next owner + this.logger.error( + `No healthy services for owner ${sampledOwner}. Trying new owner...` + ) + } else { + // Add the healthy node to the result + selectedNodes.push(healthyService) + } + } + + if (selectedNodes.length < nodeCount) { + // Reset if we fail + this.unhealthyServices = new Set([]) + this.backupServices = {} + decisionTree.push({ + stage: DECISION_TREE_STATE.FAILED_AND_RESETTING + }) + throw new Error('Not enough healthy services to choose from') + } + + // Trigger a cleanup event for all of the unhealthy and backup services, + // so they can get retried in the future + this.triggerCleanup() + + return selectedNodes + } + /** * Gets the list of services */ diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts index da3c44b0839..1935130f45d 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/types.ts @@ -13,6 +13,7 @@ export type Decision = { export enum DECISION_TREE_STATE { CHECK_SHORT_CIRCUIT = 'Check Short Circuit', GET_ALL_SERVICES = 'Get All Services', + EXCLUDE_OWNERS = 'Exclude Owners', FILTER_TO_WHITELIST = 'Filter To Whitelist', FILTER_FROM_BLACKLIST = 'Filter From Blacklist', FILTER_OUT_KNOWN_UNHEALTHY = 'Filter Out Known Unhealthy', @@ -99,4 +100,8 @@ export type DiscoveryNodeSelectorService = EventEmitterTarget & { getSelectedEndpoint: () => Promise createMiddleware: () => Middleware + getUniquelyOwnedEndpoints: ( + n: number, + excludeOwners?: string[] + ) => Promise } From 26edc99f5c23156e91d0b1300154205aa7e87f39 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:55:19 -0800 Subject: [PATCH 26/66] add challenges api to allow claiming rewards --- .../src/sdk/api/challenges/ChallengesApi.ts | 271 ++++++++++++++++++ packages/libs/src/sdk/api/challenges/types.ts | 13 + packages/libs/src/sdk/sdk.ts | 12 + 3 files changed, 296 insertions(+) create mode 100644 packages/libs/src/sdk/api/challenges/ChallengesApi.ts create mode 100644 packages/libs/src/sdk/api/challenges/types.ts diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts new file mode 100644 index 00000000000..0015b5c9115 --- /dev/null +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -0,0 +1,271 @@ +import { wAUDIO } from '@audius/fixed-decimal' +import type { + ClaimableTokens, + DiscoveryNodeSelectorService, + LoggerService +} from '../../services' +import type { + AntiAbuseOracle, + AntiAbuseOracleSelectorService +} from '../../services/AntiAbuseOracleSelector/types' +import type { RewardManager } from '../../services/Solana/programs/RewardManager/RewardManager' +import { parseParams } from '../../utils/parseParams' +import { BaseAPI, Configuration } from '../generated/default' +import { + ChallengesApi as GeneratedChallengesApi, + Configuration as ConfigurationFull +} from '../generated/full' +import type { UsersApi } from '../users/UsersApi' +import { ClaimRewardsRequest, ClaimRewardsSchema } from './types' +import type { PublicKey } from '@solana/web3.js' +import { AntiAbuseOracleApi } from '../antiAbuseOracle/AntiAbuseOracleApi' + +export class ChallengesApi extends BaseAPI { + private readonly logger: LoggerService + constructor( + config: Configuration, + private readonly usersApi: UsersApi, + private readonly discoveryNodeSelector: DiscoveryNodeSelectorService, + private readonly rewardManager: RewardManager, + private readonly claimableTokens: ClaimableTokens, + private readonly antiAbuseOracleSelector: AntiAbuseOracleSelectorService, + logger: LoggerService + ) { + super(config) + this.logger = logger.createPrefixedLogger('[challenges-api]') + } + + public async claimRewards(request: ClaimRewardsRequest) { + const args = await parseParams('claimRewards', ClaimRewardsSchema)(request) + const { challengeId, specifier, amount: inputAmount } = args + const { userId } = request + const amount = wAUDIO(inputAmount).value + const { data } = await this.usersApi.getUser({ + id: userId + }) + if (!data) { + throw new Error(`Failed to find user ${args.userId}`) + } + const { ercWallet: recipientEthAddress, handle } = data + let attestationTransactionSignatures: string[] = [] + + this.logger.debug('Creating user bank if necessary...') + const { userBank: destinationUserBank } = + await this.claimableTokens.getOrCreateUserBank({ + ethWallet: recipientEthAddress, + mint: 'wAUDIO' + }) + + this.logger.debug('Getting attestation submission state...') + const submissions = await this.rewardManager.getSubmittedAttestations({ + challengeId, + specifier + }) + this.logger.debug('Submission state:', submissions) + + let antiAbuseOracleEthAddress = submissions?.messages.find( + (m) => m.attestation.antiAbuseOracleEthAddress === null + )?.senderEthAddress + if (!antiAbuseOracleEthAddress) { + this.logger.debug('Selecting anti abuse oracle for attestation...') + const antiAbuseOracle = + await this.antiAbuseOracleSelector.getSelectedService() + if (!antiAbuseOracle) { + throw new Error('Could not find a healthy anti abuse oracle.') + } + antiAbuseOracleEthAddress = antiAbuseOracle.wallet + + this.logger.debug('Submitting anti abuse oracle attestation...') + const signature = await this.submitAntiAbuseOracleAttestation({ + antiAbuseOracle, + challengeId, + specifier, + amount, + recipientEthAddress, + handle + }) + attestationTransactionSignatures.push(signature) + } + + const existingSenderOwners = + submissions?.messages + .filter((m) => !!m.attestation.antiAbuseOracleEthAddress) + .map((m) => m.operator) ?? [] + + const state = await this.rewardManager.getRewardManagerState() + if (existingSenderOwners.length < state.minVotes) { + this.logger.debug('Submitting discovery node attestations...') + const signatures = await this.submitDiscoveryAttestations({ + userId, + antiAbuseOracleEthAddress, + challengeId, + specifier, + amount, + recipientEthAddress, + numberOfNodes: state.minVotes - existingSenderOwners.length, + excludeOwners: existingSenderOwners + }) + attestationTransactionSignatures.push(...signatures) + } + + this.logger.debug('Confirming all attestation submissions...') + await this.rewardManager.confirmAllTransactions( + attestationTransactionSignatures + ) + + this.logger.debug('Disbursing claim...') + const disbursement = await this.evaluateAttestations({ + challengeId, + specifier, + recipientEthAddress, + destinationUserBank, + antiAbuseOracleEthAddress, + amount + }) + + return disbursement + } + + private async submitAntiAbuseOracleAttestation({ + antiAbuseOracle, + challengeId, + specifier, + amount, + recipientEthAddress, + handle + }: { + antiAbuseOracle: AntiAbuseOracle + challengeId: string + specifier: string + amount: bigint + recipientEthAddress: string + handle: string + }) { + const antiAbuseOracleAttestation = await new AntiAbuseOracleApi( + new Configuration({ basePath: antiAbuseOracle.endpoint }) + ).getChallengeAttestation({ + handle, + challengeId, + specifier, + amount: Number(wAUDIO(amount).toString()) + }) + if (!antiAbuseOracleAttestation.signature) { + throw new Error('Failed to get AAO attestation') + } + const aaoSubmitSecpInstruction = + await this.rewardManager.createSubmitAttestationSecpInstruction({ + challengeId, + specifier, + amount, + recipientEthAddress, + senderEthAddress: antiAbuseOracle.wallet, + senderSignature: antiAbuseOracleAttestation.signature + }) + const aaoSubmitInstruction = + await this.rewardManager.createSubmitAttestationInstruction({ + challengeId, + specifier, + senderEthAddress: antiAbuseOracle.wallet + }) + const submitAAOTransaction = await this.rewardManager.buildTransaction({ + instructions: [aaoSubmitSecpInstruction, aaoSubmitInstruction] + }) + return await this.rewardManager.sendTransaction(submitAAOTransaction) + } + + private async submitDiscoveryAttestations({ + userId, + antiAbuseOracleEthAddress, + challengeId, + specifier, + amount, + recipientEthAddress, + numberOfNodes, + excludeOwners = [] + }: { + userId: string + antiAbuseOracleEthAddress: string + challengeId: string + specifier: string + amount: bigint + recipientEthAddress: string + numberOfNodes: number + excludeOwners: string[] + }) { + const discoveryNodes = + await this.discoveryNodeSelector.getUniquelyOwnedEndpoints( + numberOfNodes, + excludeOwners + ) + const discoveryAttestations = await Promise.all( + discoveryNodes.map((endpoint) => + new GeneratedChallengesApi( + new ConfigurationFull({ basePath: `${endpoint}/v1/full` }) + ).getChallengeAttestation({ + userId, + challengeId, + specifier, + oracle: antiAbuseOracleEthAddress + }) + ) + ) + const transactions = [] + for (const attestation of discoveryAttestations) { + const senderEthAddress = attestation.data!.ownerWallet + const senderSignature = attestation.data!.attestation + const secpInstruction = + await this.rewardManager.createSubmitAttestationSecpInstruction({ + challengeId, + specifier, + recipientEthAddress, + senderEthAddress, + antiAbuseOracleEthAddress, + amount, + senderSignature + }) + const submitInstruction = + await this.rewardManager.createSubmitAttestationInstruction({ + challengeId, + specifier, + senderEthAddress + }) + const submitTransaction = await this.rewardManager.buildTransaction({ + instructions: [secpInstruction, submitInstruction] + }) + transactions.push(submitTransaction) + } + return await Promise.all( + transactions.map((t) => this.rewardManager.sendTransaction(t)) + ) + } + + private async evaluateAttestations({ + challengeId, + specifier, + recipientEthAddress, + destinationUserBank, + antiAbuseOracleEthAddress, + amount + }: { + challengeId: string + specifier: string + recipientEthAddress: string + destinationUserBank: PublicKey + antiAbuseOracleEthAddress: string + amount: bigint + }) { + const instruction = + await this.rewardManager.createEvaluateAttestationsInstruction({ + challengeId, + specifier, + recipientEthAddress, + destinationUserBank, + antiAbuseOracleEthAddress, + amount + }) + const transaction = await this.rewardManager.buildTransaction({ + instructions: [instruction] + }) + return await this.rewardManager.sendTransaction(transaction) + } +} diff --git a/packages/libs/src/sdk/api/challenges/types.ts b/packages/libs/src/sdk/api/challenges/types.ts new file mode 100644 index 00000000000..b3dbd5f601e --- /dev/null +++ b/packages/libs/src/sdk/api/challenges/types.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' +import { HashId } from '../../types/HashId' + +export const ClaimRewardsSchema = z + .object({ + challengeId: z.string(), + specifier: z.string(), + amount: z.number(), + userId: HashId + }) + .strict() + +export type ClaimRewardsRequest = z.input diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index 1b357dd2699..ccf65b78cd0 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -41,6 +41,7 @@ import { SolanaRelayWalletAdapter } from './services/Solana/SolanaRelayWalletAda import { defaultClaimableTokensConfig } from './services/Solana/programs/ClaimableTokens/constants' import { defaultRewardManagerConfig } from './services/Solana/programs/RewardManager/constants' import { AntiAbuseOracleSelector } from './services/AntiAbuseOracleSelector/AntiAbuseOracleSelector' +import { ChallengesApi } from './api/challenges/ChallengesApi' /** * The Audius SDK @@ -224,6 +225,16 @@ const initializeApis = ({ services.auth ) + const challenges = new ChallengesApi( + generatedApiClientConfig, + users, + services.discoveryNodeSelector, + services.rewardManagerProgram, + services.claimableTokensProgram, + services.antiAbuseOracleSelector, + services.logger + ) + const generatedApiClientConfigFull = new ConfigurationFull({ fetchApi: fetch, middleware @@ -251,6 +262,7 @@ const initializeApis = ({ grants, developerApps, dashboardWalletUsers, + challenges, services } } From 6d022cb78b43fa16ef114d91ec892923b0ac4639 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:10:07 -0800 Subject: [PATCH 27/66] add missing solana wallet adapter base dep --- package-lock.json | 54 ++++++++++++++++++++++++++++++++++++++ packages/libs/package.json | 2 ++ 2 files changed, 56 insertions(+) diff --git a/package-lock.json b/package-lock.json index 11c0a35ff5e..a007fe864ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16048,6 +16048,37 @@ "@solana/web3.js": "^1.47.4" } }, + "node_modules/@solana/wallet-adapter-base": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.23.tgz", + "integrity": "sha512-apqMuYwFp1jFi55NxDfvXUX2x1T0Zh07MxhZ/nCCTGys5raSfYUh82zen2BLv8BSDj/JxZ2P/s7jrQZGrX8uAw==", + "dev": true, + "dependencies": { + "@solana/wallet-standard-features": "^1.1.0", + "@wallet-standard/base": "^1.0.1", + "@wallet-standard/features": "^1.0.3", + "eventemitter3": "^4.0.7" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.77.3" + } + }, + "node_modules/@solana/wallet-standard-features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.1.0.tgz", + "integrity": "sha512-oVyygxfYkkF5INYL0GuD8GFmNO/wd45zNesIqGCFE6X66BYxmI6HmyzQJCcZTZ0BNsezlVg4t+3MCL5AhfFoGA==", + "dev": true, + "dependencies": { + "@wallet-standard/base": "^1.0.1", + "@wallet-standard/features": "^1.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@solana/web3.js": { "version": "1.78.4", "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.78.4.tgz", @@ -40394,6 +40425,27 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/@wallet-standard/base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.0.1.tgz", + "integrity": "sha512-1To3ekMfzhYxe0Yhkpri+Fedq0SYcfrOfJi3vbLjMwF2qiKPjTGLwZkf2C9ftdQmxES+hmxhBzTwF4KgcOwf8w==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/features": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.0.3.tgz", + "integrity": "sha512-m8475I6W5LTatTZuUz5JJNK42wFRgkJTB0I9tkruMwfqBF2UN2eomkYNVf9RbrsROelCRzSFmugqjKZBFaubsA==", + "dev": true, + "dependencies": { + "@wallet-standard/base": "^1.0.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@walletconnect/browser-utils": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz", @@ -144125,6 +144177,8 @@ "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "11.1.5", + "@solana/wallet-adapter-base": "0.9.23", + "@tsconfig/node18-strictest": "1.0.0", "@types/async-retry": "1.4.4", "@types/bn.js": "5.1.0", "@types/bs58": "4.0.1", diff --git a/packages/libs/package.json b/packages/libs/package.json index 98b423d01b4..7ca34d4e573 100644 --- a/packages/libs/package.json +++ b/packages/libs/package.json @@ -135,6 +135,8 @@ "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "11.1.5", + "@solana/wallet-adapter-base": "0.9.23", + "@tsconfig/node18-strictest": "1.0.0", "@types/async-retry": "1.4.4", "@types/bn.js": "5.1.0", "@types/bs58": "4.0.1", From 68a8eb9e804fff7ca832664694e52a0562716608 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:11:15 -0800 Subject: [PATCH 28/66] change usage to prevent pulling in dep for production --- .../libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts index 96894f4fb63..385d27aa776 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts @@ -1,4 +1,4 @@ -import { +import type { SupportedTransactionVersions, TransactionOrVersionedTransaction, WalletName, @@ -23,7 +23,8 @@ export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { 'AudiusSolanaWallet' as WalletName<'AudiusSolanaWallet'> public readonly url = '' public readonly icon = '' - public readonly readyState: WalletReadyState = WalletReadyState.Loadable + public readonly readyState: WalletReadyState = + 'Loadable' as WalletReadyState.Loadable public readonly supportedTransactionVersions?: SupportedTransactionVersions private _publicKey: PublicKey | null = null From c6a5a7e30db04b551e560d222d386821e012b7d5 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:11:54 -0800 Subject: [PATCH 29/66] reconfigure SDK on web --- .../web/src/services/audius-sdk/audiusSdk.ts | 17 +++++-- .../web/src/services/audius-sdk/solana.ts | 48 +++++++++++++------ 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/web/src/services/audius-sdk/audiusSdk.ts b/packages/web/src/services/audius-sdk/audiusSdk.ts index a073f56a751..b2fa7d7ed07 100644 --- a/packages/web/src/services/audius-sdk/audiusSdk.ts +++ b/packages/web/src/services/audius-sdk/audiusSdk.ts @@ -1,12 +1,18 @@ -import { sdk, AudiusSdk, AudiusLibs } from '@audius/sdk' +import { + sdk, + AudiusSdk, + AudiusLibs, + AntiAbuseOracleSelector +} from '@audius/sdk' import { waitForLibsInit } from 'services/audius-backend/eagerLoadUtils' import { discoveryNodeSelectorService } from 'services/audius-sdk/discoveryNodeSelector' import { getStorageNodeSelector } from 'services/audius-sdk/storageNodeSelector' import { makeEntityManagerInstance } from 'services/entity-manager' +import { env } from 'services/env' import { auth } from './auth' -import { solanaService } from './solana' +import { claimableTokensService, rewardManagerService } from './solana' declare global { interface Window { @@ -35,7 +41,12 @@ const initSdk = async () => { entityManager: makeEntityManagerInstance(discoveryNodeSelector), auth, storageNodeSelector: await getStorageNodeSelector(), - solana: solanaService + claimableTokensProgram: claimableTokensService, + rewardManagerProgram: rewardManagerService, + antiAbuseOracleSelector: new AntiAbuseOracleSelector({ + endpoints: [env.AAO_ENDPOINT!], + addresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] + }) } }) console.debug('[audiusSdk] SDK initted.') diff --git a/packages/web/src/services/audius-sdk/solana.ts b/packages/web/src/services/audius-sdk/solana.ts index f4ce536bf82..ce590b2f1e0 100644 --- a/packages/web/src/services/audius-sdk/solana.ts +++ b/packages/web/src/services/audius-sdk/solana.ts @@ -1,7 +1,12 @@ -import { Solana } from '@audius/sdk' +import { + ClaimableTokens, + RewardManager, + SolanaRelay, + SolanaRelayWalletAdapter +} from '@audius/sdk' import { PublicKey } from '@solana/web3.js' -export const solanaService = new Solana({ +const solanaRelay = new SolanaRelay({ middleware: [ { pre: async (context) => { @@ -10,17 +15,30 @@ export const solanaService = new Solana({ return { url, init: context.init } } } - ], - rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, - mints: { - wAUDIO: new PublicKey(process.env.VITE_WAUDIO_MINT_ADDRESS!), - USDC: new PublicKey(process.env.VITE_USDC_MINT_ADDRESS!) - }, - programIds: { - claimableTokens: new PublicKey( - process.env.VITE_CLAIMABLE_TOKEN_PROGRAM_ADDRESS! - ), - rewardManager: new PublicKey(process.env.VITE_REWARDS_MANAGER_PROGRAM_ID!), - paymentRouter: new PublicKey(process.env.VITE_PAYMENT_ROUTER_PROGRAM_ID!) - } + ] }) + +const solanaWalletAdapter = new SolanaRelayWalletAdapter(solanaRelay) + +export const claimableTokensService = new ClaimableTokens( + { + rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, + mints: { + wAUDIO: new PublicKey(process.env.VITE_WAUDIO_MINT_ADDRESS!), + USDC: new PublicKey(process.env.VITE_USDC_MINT_ADDRESS!) + }, + programId: new PublicKey(process.env.VITE_CLAIMABLE_TOKEN_PROGRAM_ADDRESS!) + }, + solanaWalletAdapter +) + +export const rewardManagerService = new RewardManager( + { + programId: new PublicKey(process.env.VITE_REWARDS_MANAGER_PROGRAM_ID!), + rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, + rewardManagerState: new PublicKey( + process.env.VITE_REWARDS_MANAGER_PROGRAM_PDA! + ) + }, + solanaWalletAdapter +) From 6412c0e0d803e9ae08b803a7ac9914cc902e8f4e Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:23:38 -0800 Subject: [PATCH 30/66] consume changes in mobile --- packages/mobile/src/services/env.ts | 1 + packages/mobile/src/services/sdk/solana.ts | 44 +++++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/mobile/src/services/env.ts b/packages/mobile/src/services/env.ts index 2523490f4b2..6566597e774 100644 --- a/packages/mobile/src/services/env.ts +++ b/packages/mobile/src/services/env.ts @@ -20,6 +20,7 @@ export const env = { USDC_MINT_ADDRESS: Config.USDC_MINT_ADDRESS, CLAIMABLE_TOKEN_PROGRAM_ADDRESS: Config.CLAIMABLE_TOKEN_PROGRAM_ADDRESS, REWARDS_MANAGER_PROGRAM_ID: Config.REWARDS_MANAGER_PROGRAM_ID, + REWARDS_MANAGER_PROGRAM_PDA: Config.REWARDS_MANAGER_PROGRAM_PDA, PAYMENT_ROUTER_PROGRAM_ID: Config.PAYMENT_ROUTER_PROGRAM_ID ?? 'paytYpX3LPN98TAeen6bFFeraGSuWnomZmCXjAsoqPa', diff --git a/packages/mobile/src/services/sdk/solana.ts b/packages/mobile/src/services/sdk/solana.ts index 27a4de05c52..c4a50297228 100644 --- a/packages/mobile/src/services/sdk/solana.ts +++ b/packages/mobile/src/services/sdk/solana.ts @@ -1,9 +1,14 @@ -import { Solana } from '@audius/sdk' +import { + ClaimableTokens, + RewardManager, + SolanaRelay, + SolanaRelayWalletAdapter +} from '@audius/sdk' import { PublicKey } from '@solana/web3.js' import { env } from '../env' -export const solanaService = new Solana({ +const solanaRelay = new SolanaRelay({ middleware: [ { pre: async (context) => { @@ -12,15 +17,28 @@ export const solanaService = new Solana({ return { url, init: context.init } } } - ], - rpcEndpoint: process.env.SOLANA_CLUSTER_ENDPOINT, - mints: { - wAUDIO: new PublicKey(env.WAUDIO_MINT_ADDRESS!), - USDC: new PublicKey(env.USDC_MINT_ADDRESS!) - }, - programIds: { - claimableTokens: new PublicKey(env.CLAIMABLE_TOKEN_PROGRAM_ADDRESS!), - rewardManager: new PublicKey(env.REWARDS_MANAGER_PROGRAM_ID!), - paymentRouter: new PublicKey(env.PAYMENT_ROUTER_PROGRAM_ID!) - } + ] }) + +const solanaWalletAdapter = new SolanaRelayWalletAdapter(solanaRelay) + +export const claimableTokensService = new ClaimableTokens( + { + rpcEndpoint: env.SOLANA_CLUSTER_ENDPOINT, + mints: { + wAUDIO: new PublicKey(env.WAUDIO_MINT_ADDRESS!), + USDC: new PublicKey(env.USDC_MINT_ADDRESS!) + }, + programId: new PublicKey(env.CLAIMABLE_TOKEN_PROGRAM_ADDRESS!) + }, + solanaWalletAdapter +) + +export const rewardManagerService = new RewardManager( + { + programId: new PublicKey(env.REWARDS_MANAGER_PROGRAM_ID!), + rpcEndpoint: env.SOLANA_CLUSTER_ENDPOINT, + rewardManagerState: new PublicKey(env.REWARDS_MANAGER_PROGRAM_PDA!) + }, + solanaWalletAdapter +) From 7a661e43ea677b1512db8359b41bf08d45cdaabd Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:44:55 -0800 Subject: [PATCH 31/66] fix dn tests --- .../DiscoveryNodeSelector.test.ts | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts index 5d1a2ab96ee..d5ea8be5bc8 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts @@ -9,6 +9,7 @@ import type { ApiHealthResponseData, HealthCheckResponseData } from './healthCheckTypes' +import type { DiscoveryNode } from './types' // jest.mock('./healthChecks', () => ({ // getHealthCheck: jest.fn(() => ({})) @@ -42,10 +43,15 @@ const generateUnhealthyNodes = (count: number) => { return nodes } +const addDelegateOwnerWallets = (endpoint: string): DiscoveryNode => ({ + endpoint, + delegateOwnerWallet: '' +}) + const NETWORK_DISCOVERY_NODES = [ HEALTHY_NODE, ...generateSlowerHealthyNodes(10) -] +].map(addDelegateOwnerWallets) const healthyComms = { healthy: true @@ -186,12 +192,11 @@ describe('discoveryNodeSelector', () => { BEHIND_PATCH_VERSION_NODE, BEHIND_MINOR_VERSION_NODE, ...generateUnhealthyNodes(5) - ] + ].map(addDelegateOwnerWallets) }) const selected = await selector.getSelectedEndpoint() expect(selected).toBe(HEALTHY_NODE) expect(selector.isBehind).toBe(false) - expect(selector.getServices()).toStrictEqual(NETWORK_DISCOVERY_NODES) }) test('falls back to patch version backup before blockdiff backup', async () => { @@ -205,7 +210,7 @@ describe('discoveryNodeSelector', () => { BEHIND_PATCH_VERSION_NODE, BEHIND_MINOR_VERSION_NODE, UNHEALTHY_NODE - ] + ].map(addDelegateOwnerWallets) }) const selected = await selector.getSelectedEndpoint() expect(selected).toBe(BEHIND_PATCH_VERSION_NODE) @@ -221,7 +226,7 @@ describe('discoveryNodeSelector', () => { BEHIND_BLOCKDIFF_NODE, BEHIND_LARGE_BLOCKDIFF_NODE, UNHEALTHY_NODE - ] + ].map(addDelegateOwnerWallets) }) const selected = await selector.getSelectedEndpoint() expect(selected).toBe(BEHIND_BLOCKDIFF_NODE) @@ -235,7 +240,11 @@ describe('discoveryNodeSelector', () => { minVersion: '1.2.3' }, requestTimeout: 50, - bootstrapServices: [HEALTHY_NODE, UNHEALTHY_NODE, BEHIND_BLOCKDIFF_NODE] + bootstrapServices: [ + HEALTHY_NODE, + UNHEALTHY_NODE, + BEHIND_BLOCKDIFF_NODE + ].map(addDelegateOwnerWallets) }) const selected = await selector.getSelectedEndpoint() expect(selected).toBe(BEHIND_BLOCKDIFF_NODE) @@ -257,7 +266,11 @@ describe('discoveryNodeSelector', () => { minVersion: '1.2.3' }, requestTimeout: 50, - bootstrapServices: [HEALTHY_NODE, UNHEALTHY_NODE, BEHIND_BLOCKDIFF_NODE] + bootstrapServices: [ + HEALTHY_NODE, + UNHEALTHY_NODE, + BEHIND_BLOCKDIFF_NODE + ].map(addDelegateOwnerWallets) }) const selected = await selector.getSelectedEndpoint() expect(selected).toBe(BEHIND_BLOCKDIFF_NODE) @@ -276,7 +289,11 @@ describe('discoveryNodeSelector', () => { const selector = new DiscoveryNodeSelector({ initialSelectedNode: BEHIND_BLOCKDIFF_NODE, requestTimeout: 50, - bootstrapServices: [HEALTHY_NODE, UNHEALTHY_NODE, BEHIND_BLOCKDIFF_NODE] + bootstrapServices: [ + HEALTHY_NODE, + UNHEALTHY_NODE, + BEHIND_BLOCKDIFF_NODE + ].map(addDelegateOwnerWallets) }) const selected = await selector.getSelectedEndpoint() expect(selected).toBe(BEHIND_BLOCKDIFF_NODE) @@ -289,7 +306,11 @@ describe('discoveryNodeSelector', () => { initialSelectedNode: BEHIND_BLOCKDIFF_NODE, blocklist: new Set([BEHIND_BLOCKDIFF_NODE]), requestTimeout: 50, - bootstrapServices: [HEALTHY_NODE, UNHEALTHY_NODE, BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [ + HEALTHY_NODE, + UNHEALTHY_NODE, + BEHIND_BLOCKDIFF_NODE + ].map(addDelegateOwnerWallets), healthCheckThresholds: { minVersion: '1.2.3' } @@ -301,7 +322,9 @@ describe('discoveryNodeSelector', () => { test('selects fastest discovery node', async () => { const selector = new DiscoveryNodeSelector({ - bootstrapServices: [HEALTHY_NODE, ...generateSlowerHealthyNodes(5)], + bootstrapServices: [HEALTHY_NODE, ...generateSlowerHealthyNodes(5)].map( + addDelegateOwnerWallets + ), healthCheckThresholds: { minVersion: '1.2.3' } @@ -319,7 +342,7 @@ describe('discoveryNodeSelector', () => { UNHEALTHY_DATA_NODE, UNHEALTHY_NODE, UNRESPONSIVE_NODE - ], + ].map(addDelegateOwnerWallets), healthCheckThresholds: { minVersion: '1.2.3' } @@ -331,7 +354,7 @@ describe('discoveryNodeSelector', () => { describe('middleware', () => { test('prepends URL to requests', async () => { const selector = new DiscoveryNodeSelector({ - bootstrapServices: [HEALTHY_NODE] + bootstrapServices: [HEALTHY_NODE].map(addDelegateOwnerWallets) }) const middleware = selector.createMiddleware() expect(middleware.pre).not.toBeUndefined() @@ -347,7 +370,9 @@ describe('discoveryNodeSelector', () => { test('reselects if request succeeds but node fell behind', async () => { const selector = new DiscoveryNodeSelector({ initialSelectedNode: BEHIND_BLOCKDIFF_NODE, - bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE].map( + addDelegateOwnerWallets + ), healthCheckThresholds: { minVersion: '1.2.3' } @@ -379,7 +404,7 @@ describe('discoveryNodeSelector', () => { test("doesn't reselect if behind but was already behind", async () => { const selector = new DiscoveryNodeSelector({ - bootstrapServices: [BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [BEHIND_BLOCKDIFF_NODE].map(addDelegateOwnerWallets), healthCheckThresholds: { minVersion: '1.2.3' } @@ -415,7 +440,9 @@ describe('discoveryNodeSelector', () => { test('reselects if request fails and node fell behind', async () => { const selector = new DiscoveryNodeSelector({ initialSelectedNode: BEHIND_BLOCKDIFF_NODE, - bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE].map( + addDelegateOwnerWallets + ), healthCheckThresholds: { minVersion: '1.2.3' } @@ -458,7 +485,9 @@ describe('discoveryNodeSelector', () => { test('reselects if request fails and node unhealthy', async () => { const selector = new DiscoveryNodeSelector({ initialSelectedNode: UNHEALTHY_NODE, - bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE].map( + addDelegateOwnerWallets + ), healthCheckThresholds: { minVersion: '1.2.3' } @@ -500,7 +529,9 @@ describe('discoveryNodeSelector', () => { test("doesn't reselect if request fails but node is healthy", async () => { const selector = new DiscoveryNodeSelector({ initialSelectedNode: HEALTHY_NODE, - bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE].map( + addDelegateOwnerWallets + ), healthCheckThresholds: { minVersion: '1.2.3' } @@ -530,7 +561,7 @@ describe('discoveryNodeSelector', () => { test('resets isBehind when request shows the node is caught up', async () => { const selector = new DiscoveryNodeSelector({ - bootstrapServices: [BEHIND_BLOCKDIFF_NODE], + bootstrapServices: [BEHIND_BLOCKDIFF_NODE].map(addDelegateOwnerWallets), healthCheckThresholds: { minVersion: '1.2.3' } From 2276a99f063e12abcd98c6f3cf29b076963f0a35 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Sat, 23 Dec 2023 01:00:04 -0800 Subject: [PATCH 32/66] fix other tests --- .../libs/src/sdk/api/users/UsersApi.test.ts | 36 +++++++++++++------ .../StorageNodeSelector.test.ts | 7 ++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index e3e4c1ebce2..cd3e7e0e47e 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -12,13 +12,19 @@ import { StorageNodeSelector } from '../../services/StorageNodeSelector' import { Configuration } from '../generated/default' import { UsersApi } from './UsersApi' -import { Solana } from '../../services' -import { ClaimableTokensProgram } from '@audius/spl' import { PublicKey, Secp256k1Program, - TransactionMessage + TransactionInstruction, + TransactionMessage, + VersionedTransaction } from '@solana/web3.js' +import { ClaimableTokensProgram } from '@audius/spl' +import { + ClaimableTokens, + SolanaRelay, + SolanaRelayWalletAdapter +} from '../../services' const pngFile = fs.readFileSync( path.resolve(__dirname, '../../test/png-file.png') @@ -61,7 +67,11 @@ describe('UsersApi', () => { discoveryNodeSelector, logger }) - const solana = new Solana() + const solanaRelay = new SolanaRelay() + const claimableTokens = new ClaimableTokens( + {}, + new SolanaRelayWalletAdapter(solanaRelay) + ) beforeAll(() => { users = new UsersApi( @@ -71,7 +81,7 @@ describe('UsersApi', () => { new EntityManager({ discoveryNodeSelector: new DiscoveryNodeSelector() }), auth, new Logger(), - solana + claimableTokens ) jest.spyOn(console, 'warn').mockImplementation(() => {}) jest.spyOn(console, 'info').mockImplementation(() => {}) @@ -259,10 +269,16 @@ describe('UsersApi', () => { // Turn the relay call into a bunch of assertions on the final transaction jest - .spyOn(Solana.prototype, 'relay') + .spyOn(SolanaRelay.prototype, 'relay') .mockImplementation(async ({ transaction }) => { - const message = TransactionMessage.decompile(transaction.message) - const [secp, transfer] = message.instructions + let instructions: TransactionInstruction[] = [] + if (transaction instanceof VersionedTransaction) { + const message = TransactionMessage.decompile(transaction.message) + instructions = message.instructions + } else { + instructions = transaction.instructions + } + const [secp, transfer] = instructions expect(secp?.programId.toBase58()).toBe( Secp256k1Program.programId.toBase58() ) @@ -292,7 +308,7 @@ describe('UsersApi', () => { // Mock getFeePayer jest - .spyOn(Solana.prototype, 'getFeePayer') + .spyOn(SolanaRelay.prototype, 'getFeePayer') .mockImplementation(async () => { return feePayer }) @@ -321,7 +337,7 @@ describe('UsersApi', () => { }) // Ensure relay was attempted to ensure the assertions run - expect(solana.relay).toHaveBeenCalledTimes(1) + expect(solanaRelay.relay).toHaveBeenCalledTimes(1) }) }) }) diff --git a/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts b/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts index e0a5c1b5ce1..ec2741e3a40 100644 --- a/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts @@ -51,7 +51,7 @@ const mswHandlers = [ version: '1.2.3', block_difference: 0, network: { - discovery_nodes: [discoveryNode], + discovery_nodes: [{ endpoint: discoveryNode, delegateOwnerWallet: '' }], content_nodes: [storageNodeA, storageNodeB] } } @@ -150,7 +150,10 @@ describe('StorageNodeSelector', () => { }) it('selects correct storage node when discovery node is selected', async () => { - const bootstrapDiscoveryNodes = [discoveryNode] + const bootstrapDiscoveryNodes = [discoveryNode].map((endpoint) => ({ + endpoint, + delegateOwnerWallet: '' + })) const discoveryNodeSelector = new DiscoveryNodeSelector({ healthCheckThresholds: { minVersion: '1.2.3' From 4049d23a9d55b90ca7a9d5e0e4e3912a2df5c3b4 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:39:02 -0600 Subject: [PATCH 33/66] rename things --- .../src/sdk/api/challenges/ChallengesApi.ts | 8 ++--- .../libs/src/sdk/api/users/UsersApi.test.ts | 4 +-- packages/libs/src/sdk/api/users/UsersApi.ts | 4 +-- packages/libs/src/sdk/sdk.ts | 32 ++++++++++--------- .../Solana/SolanaRelayWalletAdapter.ts | 6 +++- .../libs/src/sdk/services/Solana/index.ts | 4 +-- ...{SolanaProgram.ts => BaseSolanaProgram.ts} | 6 ++-- .../ClaimableTokensClient.ts} | 13 +++----- .../constants.ts | 0 .../types.ts | 12 +++++-- .../programs/RewardManager/constants.ts | 10 ------ .../RewardManagerClient.ts} | 14 ++++---- .../programs/RewardManagerClient/constants.ts | 11 +++++++ .../types.ts | 13 +++++--- .../src/sdk/services/Solana/programs/types.ts | 2 +- packages/libs/src/sdk/types.ts | 8 ++--- 16 files changed, 80 insertions(+), 67 deletions(-) rename packages/libs/src/sdk/services/Solana/programs/{SolanaProgram.ts => BaseSolanaProgram.ts} (96%) rename packages/libs/src/sdk/services/Solana/programs/{ClaimableTokens/ClaimableTokens.ts => ClaimableTokensClient/ClaimableTokensClient.ts} (94%) rename packages/libs/src/sdk/services/Solana/programs/{ClaimableTokens => ClaimableTokensClient}/constants.ts (100%) rename packages/libs/src/sdk/services/Solana/programs/{ClaimableTokens => ClaimableTokensClient}/types.ts (91%) delete mode 100644 packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts rename packages/libs/src/sdk/services/Solana/programs/{RewardManager/RewardManager.ts => RewardManagerClient/RewardManagerClient.ts} (95%) create mode 100644 packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts rename packages/libs/src/sdk/services/Solana/programs/{RewardManager => RewardManagerClient}/types.ts (83%) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 0015b5c9115..9bff8db77f1 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -1,6 +1,6 @@ import { wAUDIO } from '@audius/fixed-decimal' import type { - ClaimableTokens, + ClaimableTokensClient, DiscoveryNodeSelectorService, LoggerService } from '../../services' @@ -8,7 +8,7 @@ import type { AntiAbuseOracle, AntiAbuseOracleSelectorService } from '../../services/AntiAbuseOracleSelector/types' -import type { RewardManager } from '../../services/Solana/programs/RewardManager/RewardManager' +import type { RewardManagerClient } from '../../services/Solana/programs/RewardManagerClient/RewardManagerClient' import { parseParams } from '../../utils/parseParams' import { BaseAPI, Configuration } from '../generated/default' import { @@ -26,8 +26,8 @@ export class ChallengesApi extends BaseAPI { config: Configuration, private readonly usersApi: UsersApi, private readonly discoveryNodeSelector: DiscoveryNodeSelectorService, - private readonly rewardManager: RewardManager, - private readonly claimableTokens: ClaimableTokens, + private readonly rewardManager: RewardManagerClient, + private readonly claimableTokens: ClaimableTokensClient, private readonly antiAbuseOracleSelector: AntiAbuseOracleSelectorService, logger: LoggerService ) { diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index cd3e7e0e47e..f945acad6df 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -21,7 +21,7 @@ import { } from '@solana/web3.js' import { ClaimableTokensProgram } from '@audius/spl' import { - ClaimableTokens, + ClaimableTokensClient, SolanaRelay, SolanaRelayWalletAdapter } from '../../services' @@ -68,7 +68,7 @@ describe('UsersApi', () => { logger }) const solanaRelay = new SolanaRelay() - const claimableTokens = new ClaimableTokens( + const claimableTokens = new ClaimableTokensClient( {}, new SolanaRelayWalletAdapter(solanaRelay) ) diff --git a/packages/libs/src/sdk/api/users/UsersApi.ts b/packages/libs/src/sdk/api/users/UsersApi.ts index 29f1a65365a..66c68fd5725 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.ts @@ -38,7 +38,7 @@ import { SendTipRequest, SendTipSchema } from './types' -import type { ClaimableTokens } from '../../services/Solana/programs/ClaimableTokens/ClaimableTokens' +import type { ClaimableTokensClient } from '../../services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient' export class UsersApi extends GeneratedUsersApi { constructor( @@ -48,7 +48,7 @@ export class UsersApi extends GeneratedUsersApi { private readonly entityManager: EntityManagerService, private readonly auth: AuthService, private readonly logger: LoggerService, - private readonly claimableTokens: ClaimableTokens + private readonly claimableTokens: ClaimableTokensClient ) { super(configuration) this.logger = logger.createPrefixedLogger('[users-api]') diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index ccf65b78cd0..4a0bad6a98a 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -29,17 +29,17 @@ import { Storage, EntityManager, AppAuth, - RewardManager + RewardManagerClient } from './services' import { defaultEntityManagerConfig } from './services/EntityManager/constants' import { Logger } from './services/Logger' import { StorageNodeSelector } from './services/StorageNodeSelector' import { SdkConfig, SdkConfigSchema, ServicesContainer } from './types' import { SolanaRelay } from './services/Solana/SolanaRelay' -import { ClaimableTokens } from './services/Solana/programs/ClaimableTokens/ClaimableTokens' +import { ClaimableTokensClient } from './services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient' import { SolanaRelayWalletAdapter } from './services/Solana/SolanaRelayWalletAdapter' -import { defaultClaimableTokensConfig } from './services/Solana/programs/ClaimableTokens/constants' -import { defaultRewardManagerConfig } from './services/Solana/programs/RewardManager/constants' +import { defaultClaimableTokensConfig } from './services/Solana/programs/ClaimableTokensClient/constants' +import { defaultRewardManagerClentConfig } from './services/Solana/programs/RewardManagerClient/constants' import { AntiAbuseOracleSelector } from './services/AntiAbuseOracleSelector/AntiAbuseOracleSelector' import { ChallengesApi } from './api/challenges/ChallengesApi' @@ -118,19 +118,21 @@ const initializeServices = (config: SdkConfig) => { ] }) - const defaultSolanaWalletAdapter = new SolanaRelayWalletAdapter( - config.services?.solanaRelay ?? defaultSolanaRelay - ) + const defaultSolanaWalletAdapter = new SolanaRelayWalletAdapter({ + solanaRelay: config.services?.solanaRelay ?? defaultSolanaRelay + }) - const defaultClaimableTokensProgram = new ClaimableTokens( - defaultClaimableTokensConfig, - config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter - ) + const defaultClaimableTokensProgram = new ClaimableTokensClient({ + ...defaultClaimableTokensConfig, + solanaWalletAdapter: + config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter + }) - const defaultRewardManagerProgram = new RewardManager( - defaultRewardManagerConfig, - config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter - ) + const defaultRewardManagerProgram = new RewardManagerClient({ + ...defaultRewardManagerClentConfig, + solanaWalletAdapter: + config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter + }) const defaultServices: ServicesContainer = { storageNodeSelector, diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts index 385d27aa776..5c8226a2b97 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts @@ -27,11 +27,15 @@ export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { 'Loadable' as WalletReadyState.Loadable public readonly supportedTransactionVersions?: SupportedTransactionVersions + private readonly solanaRelay: SolanaRelay + private _publicKey: PublicKey | null = null private _connecting = false private _connected = false - constructor(private readonly solanaRelay: SolanaRelay) {} + constructor({ solanaRelay }: { solanaRelay: SolanaRelay }) { + this.solanaRelay = solanaRelay + } public get publicKey() { return this._publicKey diff --git a/packages/libs/src/sdk/services/Solana/index.ts b/packages/libs/src/sdk/services/Solana/index.ts index 59583f979c3..62e54c66f3c 100644 --- a/packages/libs/src/sdk/services/Solana/index.ts +++ b/packages/libs/src/sdk/services/Solana/index.ts @@ -1,5 +1,5 @@ export * from './SolanaRelay' export * from './SolanaRelayWalletAdapter' -export * from './programs/ClaimableTokens/ClaimableTokens' -export * from './programs/RewardManager/RewardManager' +export * from './programs/ClaimableTokensClient/ClaimableTokensClient' +export * from './programs/RewardManagerClient/RewardManagerClient' export * from './types' diff --git a/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts b/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts similarity index 96% rename from packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts rename to packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts index 01bcb09c5fc..8a5f4a3f42e 100644 --- a/packages/libs/src/sdk/services/Solana/programs/SolanaProgram.ts +++ b/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts @@ -9,7 +9,7 @@ import type { SolanaWalletAdapter } from '../types' import { BuildTransactionRequest, BuildTransactionSchema, - type SolanaProgramConfigInternal + type BaseSolanaProgramConfigInternal } from './types' import { parseParams } from '../../../utils/parseParams' @@ -19,11 +19,11 @@ const isPublicKeyArray = (arr: any[]): arr is PublicKey[] => /** * Abstract class for initializing individual program clients. */ -export class SolanaProgram { +export class BaseSolanaProgram { /** The endpoint for the Solana RPC. */ protected readonly connection: Connection constructor( - config: SolanaProgramConfigInternal, + config: BaseSolanaProgramConfigInternal, protected wallet: SolanaWalletAdapter ) { this.connection = new Connection(config.rpcEndpoint, config.rpcConfig) diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/ClaimableTokens.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts similarity index 94% rename from packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/ClaimableTokens.ts rename to packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts index 4b3be2616b6..c5122d8d9d7 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/ClaimableTokens.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts @@ -7,7 +7,7 @@ import { PublicKey } from '@solana/web3.js' import { parseParams } from '../../../../utils/parseParams' -import type { Mint, SolanaWalletAdapter } from '../../types' +import type { Mint } from '../../types' import { type GetOrCreateUserBankRequest, @@ -20,11 +20,11 @@ import { ClaimableTokensConfig } from './types' -import { SolanaProgram } from '../SolanaProgram' +import { BaseSolanaProgram } from '../BaseSolanaProgram' import { defaultClaimableTokensConfig } from './constants' import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' -export class ClaimableTokens extends SolanaProgram { +export class ClaimableTokensClient extends BaseSolanaProgram { /** The program ID of the ClaimableTokensProgram instance. */ private readonly programId: PublicKey /** Map from token mint name to public key address. */ @@ -32,15 +32,12 @@ export class ClaimableTokens extends SolanaProgram { /** Map from token mint name to derived user bank authority. */ private readonly authorities: Record - constructor( - config: ClaimableTokensConfig, - solanaWalletAdapter: SolanaWalletAdapter - ) { + constructor(config: ClaimableTokensConfig) { const configWithDefaults = mergeConfigWithDefaults( config, defaultClaimableTokensConfig ) - super(configWithDefaults, solanaWalletAdapter) + super(configWithDefaults, config.solanaWalletAdapter) this.programId = configWithDefaults.programId this.mints = configWithDefaults.mints this.authorities = { diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/constants.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/constants.ts similarity index 100% rename from packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/constants.ts rename to packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/constants.ts diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts similarity index 91% rename from packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts rename to packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts index a0750a8110b..e1a1135086c 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokens/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts @@ -1,20 +1,26 @@ import { z } from 'zod' import type { AuthService } from '../../../Auth' -import { Mint, MintSchema, PublicKeySchema } from '../../types' +import { + Mint, + MintSchema, + PublicKeySchema, + SolanaWalletAdapter +} from '../../types' import type { PublicKey } from '@solana/web3.js' import type { Prettify } from '../../../../utils/prettify' -import type { SolanaProgramConfigInternal } from '../types' +import type { BaseSolanaProgramConfigInternal } from '../types' export type ClaimableTokensConfigInternal = { /** The program ID of the ClaimableTokensProgram instance. */ programId: PublicKey /** Map from token mint name to public key address. */ mints: Record -} & SolanaProgramConfigInternal +} & BaseSolanaProgramConfigInternal export type ClaimableTokensConfig = Prettify< Partial> & { mints?: Prettify>> + solanaWalletAdapter: SolanaWalletAdapter } > diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts deleted file mode 100644 index 62480716942..00000000000 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManager/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import type { RewardManagerConfigInternal } from './types' - -export const defaultRewardManagerConfig: RewardManagerConfigInternal = { - programId: new PublicKey('Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ'), - rpcEndpoint: 'https://api.mainnet-beta.solana.com', - rewardManagerState: new PublicKey( - '3V9opXNpHmPPymKeq7CYD8wWMH8wzFXmqEkNdzfsZhYq' - ) -} diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts similarity index 95% rename from packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts rename to packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts index 433984a5d82..dbdefa08510 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManager/RewardManager.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts @@ -1,12 +1,12 @@ -import { RewardManagerProgram, ethAddress } from '@audius/spl' +import { RewardManagerProgram } from '@audius/spl' import type { SolanaWalletAdapter } from '../../types' -import { SolanaProgram } from '../SolanaProgram' +import { BaseSolanaProgram } from '../BaseSolanaProgram' import { CreateEvaluateAttestationsInstructionRequest, CreateEvaluateAttestationsInstructionSchema, CreateSubmitAttestationInstructionSchema, type CreateSubmitAttestationRequest, - type RewardManagerConfig, + type RewardManagerClientConfig, CreateSenderInstructionRequest, CreateSenderInstructionSchema, CreateSubmitAttestationSecpInstructionRequest, @@ -18,21 +18,21 @@ import { Secp256k1Program, type PublicKey } from '@solana/web3.js' import { parseParams } from '../../../../utils/parseParams' import type { RewardManagerStateData } from '@audius/spl/dist/types/reward-manager/types' import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' -import { defaultRewardManagerConfig } from './constants' +import { defaultRewardManagerClentConfig } from './constants' -export class RewardManager extends SolanaProgram { +export class RewardManagerClient extends BaseSolanaProgram { private readonly programId: PublicKey private readonly rewardManagerStateAccount: PublicKey private readonly authority: PublicKey private rewardManagerState: RewardManagerStateData | null = null constructor( - config: RewardManagerConfig, + config: RewardManagerClientConfig, solanaWalletAdapter: SolanaWalletAdapter ) { const configWithDefaults = mergeConfigWithDefaults( config, - defaultRewardManagerConfig + defaultRewardManagerClentConfig ) super(configWithDefaults, solanaWalletAdapter) this.programId = configWithDefaults.programId diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts new file mode 100644 index 00000000000..742ab052fdf --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts @@ -0,0 +1,11 @@ +import { PublicKey } from '@solana/web3.js' +import type { RewardManagerClientConfigInternal } from './types' + +export const defaultRewardManagerClentConfig: RewardManagerClientConfigInternal = + { + programId: new PublicKey('Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ'), + rpcEndpoint: 'https://api.mainnet-beta.solana.com', + rewardManagerState: new PublicKey( + '3V9opXNpHmPPymKeq7CYD8wWMH8wzFXmqEkNdzfsZhYq' + ) + } diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts similarity index 83% rename from packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts rename to packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts index 157a8982013..54fe5e89bd8 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManager/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts @@ -1,14 +1,17 @@ import type { PublicKey } from '@solana/web3.js' -import type { SolanaProgramConfigInternal } from '../types' +import type { BaseSolanaProgramConfigInternal } from '../types' import { z } from 'zod' -import { PublicKeySchema } from '../../types' +import { PublicKeySchema, SolanaWalletAdapter } from '../../types' -export type RewardManagerConfigInternal = { +export type RewardManagerClientConfigInternal = { programId: PublicKey rewardManagerState: PublicKey -} & SolanaProgramConfigInternal +} & BaseSolanaProgramConfigInternal -export type RewardManagerConfig = Partial +export type RewardManagerClientConfig = + Partial & { + solanaWalletAdapter: SolanaWalletAdapter + } export const CreateSenderInstructionSchema = z.object({ manager: PublicKeySchema, diff --git a/packages/libs/src/sdk/services/Solana/programs/types.ts b/packages/libs/src/sdk/services/Solana/programs/types.ts index db070248a96..c6a1fcac4da 100644 --- a/packages/libs/src/sdk/services/Solana/programs/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/types.ts @@ -6,7 +6,7 @@ import { import { z } from 'zod' import { PublicKeySchema } from '../types' -export type SolanaProgramConfigInternal = { +export type BaseSolanaProgramConfigInternal = { /** Connection to interact with the Solana RPC */ rpcEndpoint: string /** Configuration to use for the RPC connection. */ diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index b00899ba92a..ede5fda0b3a 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -7,8 +7,8 @@ import type { LoggerService } from './services/Logger' import type { StorageService } from './services/Storage' import type { StorageNodeSelectorService } from './services/StorageNodeSelector' import type { - RewardManager, - ClaimableTokens, + RewardManagerClient, + ClaimableTokensClient, SolanaRelayService, SolanaWalletAdapter } from './services/Solana' @@ -58,12 +58,12 @@ export type ServicesContainer = { /** * Claimable Tokens Program client for Solana */ - claimableTokensProgram: ClaimableTokens + claimableTokensProgram: ClaimableTokensClient /** * Reward Manager Program client for Solana */ - rewardManagerProgram: RewardManager + rewardManagerProgram: RewardManagerClient /** * Service used to choose a healthy Anti Abuse Oracle From 0d9595abc15eb6554c0a32f4e782bbf6cb82320e Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:42:01 -0600 Subject: [PATCH 34/66] remove unnecessary dep, fix some errors --- package-lock.json | 1 - .../plugins/pedalboard/apps/relay/src/scripts/sandbox.ts | 2 +- packages/libs/package.json | 1 - packages/libs/src/sdk/api/users/UsersApi.test.ts | 7 +++---- .../programs/RewardManagerClient/RewardManagerClient.ts | 8 ++------ 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index a007fe864ae..69115784f00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -144178,7 +144178,6 @@ "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "11.1.5", "@solana/wallet-adapter-base": "0.9.23", - "@tsconfig/node18-strictest": "1.0.0", "@types/async-retry": "1.4.4", "@types/bn.js": "5.1.0", "@types/bs58": "4.0.1", diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/scripts/sandbox.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/scripts/sandbox.ts index eb66e0c457a..e95c938c45c 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/scripts/sandbox.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/scripts/sandbox.ts @@ -27,7 +27,7 @@ export const main = async () => { console.log(`using ${initialSelectedNode}`); const discoveryNodeSelector = new DiscoveryNodeSelector({ - initialSelectedNode, + initialSelectedNode: initialSelectedNode.endpoint, }); const entityManager = new EntityManager({ diff --git a/packages/libs/package.json b/packages/libs/package.json index 7ca34d4e573..1d8bc4ce3b5 100644 --- a/packages/libs/package.json +++ b/packages/libs/package.json @@ -136,7 +136,6 @@ "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "11.1.5", "@solana/wallet-adapter-base": "0.9.23", - "@tsconfig/node18-strictest": "1.0.0", "@types/async-retry": "1.4.4", "@types/bn.js": "5.1.0", "@types/bs58": "4.0.1", diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index f945acad6df..82a30eeb98a 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -68,10 +68,9 @@ describe('UsersApi', () => { logger }) const solanaRelay = new SolanaRelay() - const claimableTokens = new ClaimableTokensClient( - {}, - new SolanaRelayWalletAdapter(solanaRelay) - ) + const claimableTokens = new ClaimableTokensClient({ + solanaWalletAdapter: new SolanaRelayWalletAdapter({ solanaRelay }) + }) beforeAll(() => { users = new UsersApi( diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts index dbdefa08510..554b707301b 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts @@ -1,5 +1,4 @@ import { RewardManagerProgram } from '@audius/spl' -import type { SolanaWalletAdapter } from '../../types' import { BaseSolanaProgram } from '../BaseSolanaProgram' import { CreateEvaluateAttestationsInstructionRequest, @@ -26,15 +25,12 @@ export class RewardManagerClient extends BaseSolanaProgram { private readonly authority: PublicKey private rewardManagerState: RewardManagerStateData | null = null - constructor( - config: RewardManagerClientConfig, - solanaWalletAdapter: SolanaWalletAdapter - ) { + constructor(config: RewardManagerClientConfig) { const configWithDefaults = mergeConfigWithDefaults( config, defaultRewardManagerClentConfig ) - super(configWithDefaults, solanaWalletAdapter) + super(configWithDefaults, config.solanaWalletAdapter) this.programId = configWithDefaults.programId this.rewardManagerStateAccount = configWithDefaults.rewardManagerState this.authority = RewardManagerProgram.deriveAuthority({ From b3581f9c3c6c850d7abc2a5c56993b65a4a449c6 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:43:16 -0600 Subject: [PATCH 35/66] fix client configuration --- .../mobile/src/services/sdk/audius-sdk.ts | 13 +++++-- packages/mobile/src/services/sdk/solana.ts | 34 ++++++++--------- .../web/src/services/audius-sdk/solana.ts | 38 +++++++++---------- 3 files changed, 42 insertions(+), 43 deletions(-) diff --git a/packages/mobile/src/services/sdk/audius-sdk.ts b/packages/mobile/src/services/sdk/audius-sdk.ts index 5c657e66e0f..5422e55c56a 100644 --- a/packages/mobile/src/services/sdk/audius-sdk.ts +++ b/packages/mobile/src/services/sdk/audius-sdk.ts @@ -1,11 +1,13 @@ import { EventEmitter } from 'events' import type { AudiusSdk } from '@audius/sdk' -import { sdk } from '@audius/sdk' +import { AntiAbuseOracleSelector, sdk } from '@audius/sdk' + +import { env } from '../env' import { auth } from './auth' import { discoveryNodeSelectorService } from './discoveryNodeSelector' -import { solanaService } from './solana' +import { claimableTokensService, rewardManagerService } from './solana' import { getStorageNodeSelector } from './storageNodeSelector' let inProgress = false @@ -22,7 +24,12 @@ const initSdk = async () => { discoveryNodeSelector: await discoveryNodeSelectorService.getInstance(), auth, storageNodeSelector: await getStorageNodeSelector(), - solana: solanaService + claimableTokensProgram: claimableTokensService, + rewardManagerProgram: rewardManagerService, + antiAbuseOracleSelector: new AntiAbuseOracleSelector({ + endpoints: [env.AAO_ENDPOINT], + addresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] + }) } }) sdkInstance = audiusSdk diff --git a/packages/mobile/src/services/sdk/solana.ts b/packages/mobile/src/services/sdk/solana.ts index c4a50297228..941e2d2e32b 100644 --- a/packages/mobile/src/services/sdk/solana.ts +++ b/packages/mobile/src/services/sdk/solana.ts @@ -1,6 +1,6 @@ import { - ClaimableTokens, - RewardManager, + ClaimableTokensClient, + RewardManagerClient, SolanaRelay, SolanaRelayWalletAdapter } from '@audius/sdk' @@ -20,25 +20,21 @@ const solanaRelay = new SolanaRelay({ ] }) -const solanaWalletAdapter = new SolanaRelayWalletAdapter(solanaRelay) +const solanaWalletAdapter = new SolanaRelayWalletAdapter({ solanaRelay }) -export const claimableTokensService = new ClaimableTokens( - { - rpcEndpoint: env.SOLANA_CLUSTER_ENDPOINT, - mints: { - wAUDIO: new PublicKey(env.WAUDIO_MINT_ADDRESS!), - USDC: new PublicKey(env.USDC_MINT_ADDRESS!) - }, - programId: new PublicKey(env.CLAIMABLE_TOKEN_PROGRAM_ADDRESS!) +export const claimableTokensService = new ClaimableTokensClient({ + rpcEndpoint: env.SOLANA_CLUSTER_ENDPOINT, + mints: { + wAUDIO: new PublicKey(env.WAUDIO_MINT_ADDRESS!), + USDC: new PublicKey(env.USDC_MINT_ADDRESS!) }, + programId: new PublicKey(env.CLAIMABLE_TOKEN_PROGRAM_ADDRESS!), solanaWalletAdapter -) +}) -export const rewardManagerService = new RewardManager( - { - programId: new PublicKey(env.REWARDS_MANAGER_PROGRAM_ID!), - rpcEndpoint: env.SOLANA_CLUSTER_ENDPOINT, - rewardManagerState: new PublicKey(env.REWARDS_MANAGER_PROGRAM_PDA!) - }, +export const rewardManagerService = new RewardManagerClient({ + programId: new PublicKey(env.REWARDS_MANAGER_PROGRAM_ID!), + rpcEndpoint: env.SOLANA_CLUSTER_ENDPOINT, + rewardManagerState: new PublicKey(env.REWARDS_MANAGER_PROGRAM_PDA!), solanaWalletAdapter -) +}) diff --git a/packages/web/src/services/audius-sdk/solana.ts b/packages/web/src/services/audius-sdk/solana.ts index ce590b2f1e0..930361084d4 100644 --- a/packages/web/src/services/audius-sdk/solana.ts +++ b/packages/web/src/services/audius-sdk/solana.ts @@ -1,6 +1,6 @@ import { - ClaimableTokens, - RewardManager, + ClaimableTokensClient, + RewardManagerClient, SolanaRelay, SolanaRelayWalletAdapter } from '@audius/sdk' @@ -18,27 +18,23 @@ const solanaRelay = new SolanaRelay({ ] }) -const solanaWalletAdapter = new SolanaRelayWalletAdapter(solanaRelay) +const solanaWalletAdapter = new SolanaRelayWalletAdapter({ solanaRelay }) -export const claimableTokensService = new ClaimableTokens( - { - rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, - mints: { - wAUDIO: new PublicKey(process.env.VITE_WAUDIO_MINT_ADDRESS!), - USDC: new PublicKey(process.env.VITE_USDC_MINT_ADDRESS!) - }, - programId: new PublicKey(process.env.VITE_CLAIMABLE_TOKEN_PROGRAM_ADDRESS!) +export const claimableTokensService = new ClaimableTokensClient({ + rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, + mints: { + wAUDIO: new PublicKey(process.env.VITE_WAUDIO_MINT_ADDRESS!), + USDC: new PublicKey(process.env.VITE_USDC_MINT_ADDRESS!) }, + programId: new PublicKey(process.env.VITE_CLAIMABLE_TOKEN_PROGRAM_ADDRESS!), solanaWalletAdapter -) +}) -export const rewardManagerService = new RewardManager( - { - programId: new PublicKey(process.env.VITE_REWARDS_MANAGER_PROGRAM_ID!), - rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, - rewardManagerState: new PublicKey( - process.env.VITE_REWARDS_MANAGER_PROGRAM_PDA! - ) - }, +export const rewardManagerService = new RewardManagerClient({ + programId: new PublicKey(process.env.VITE_REWARDS_MANAGER_PROGRAM_ID!), + rpcEndpoint: process.env.VITE_SOLANA_CLUSTER_ENDPOINT, + rewardManagerState: new PublicKey( + process.env.VITE_REWARDS_MANAGER_PROGRAM_PDA! + ), solanaWalletAdapter -) +}) From 407594d15d32bd2577d5e6ce8326b2b7f11b0315 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:44:42 -0600 Subject: [PATCH 36/66] remove lower() in favor of the sdk doing checksums --- packages/discovery-provider/src/api/v1/challenges.py | 8 ++------ .../discovery-provider/src/queries/get_attestation.py | 7 +++---- packages/libs/src/sdk/api/challenges/ChallengesApi.ts | 7 ++++++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/discovery-provider/src/api/v1/challenges.py b/packages/discovery-provider/src/api/v1/challenges.py index eaf7d6ca79c..da1cb79c0e4 100644 --- a/packages/discovery-provider/src/api/v1/challenges.py +++ b/packages/discovery-provider/src/api/v1/challenges.py @@ -88,7 +88,7 @@ def get(self, challenge_id: str): db = get_db_read_replica() with db.scoped_session() as session: try: - owner_wallet, signature, bytes = get_attestation( + owner_wallet, signature = get_attestation( session, user_id=decoded_user_id, oracle_address=oracle_address, @@ -97,11 +97,7 @@ def get(self, challenge_id: str): ) return success_response( - { - "owner_wallet": owner_wallet, - "attestation": signature, - "bytes": bytes.hex(), - } + {"owner_wallet": owner_wallet, "attestation": signature} ) except AttestationError as e: abort(400, e) diff --git a/packages/discovery-provider/src/queries/get_attestation.py b/packages/discovery-provider/src/queries/get_attestation.py index d96a03006cb..bb75fa64ceb 100644 --- a/packages/discovery-provider/src/queries/get_attestation.py +++ b/packages/discovery-provider/src/queries/get_attestation.py @@ -1,4 +1,3 @@ -import logging from datetime import datetime from typing import Tuple @@ -104,7 +103,7 @@ def is_valid_oracle(address: str) -> bool: oracle_addresses = oracle_addresses.decode().split(",") else: oracle_addresses = get_oracle_addresses_from_chain(redis) - return address.lower() in [a.lower() for a in oracle_addresses] + return address in oracle_addresses def sign_attestation(attestation_bytes: bytes, private_key: str): @@ -194,8 +193,8 @@ def get_attestation( attestation = Attestation( amount=str(user_challenge.amount), - oracle_address=oracle_address.lower(), - user_address=user_address.lower(), + oracle_address=oracle_address, + user_address=user_address, challenge_id=challenge.id, challenge_specifier=user_challenge.specifier, ) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 9bff8db77f1..1382ed58ed5 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -19,6 +19,7 @@ import type { UsersApi } from '../users/UsersApi' import { ClaimRewardsRequest, ClaimRewardsSchema } from './types' import type { PublicKey } from '@solana/web3.js' import { AntiAbuseOracleApi } from '../antiAbuseOracle/AntiAbuseOracleApi' +import { toChecksumAddress } from 'ethereumjs-util' export class ChallengesApi extends BaseAPI { private readonly logger: LoggerService @@ -85,6 +86,9 @@ export class ChallengesApi extends BaseAPI { handle }) attestationTransactionSignatures.push(signature) + } else { + // Need to convert to checksum address as the attestation is lowercased + antiAbuseOracleEthAddress = toChecksumAddress(antiAbuseOracleEthAddress) } const existingSenderOwners = @@ -110,7 +114,8 @@ export class ChallengesApi extends BaseAPI { this.logger.debug('Confirming all attestation submissions...') await this.rewardManager.confirmAllTransactions( - attestationTransactionSignatures + attestationTransactionSignatures, + 'finalized' // for some reason, only works when finalized... ) this.logger.debug('Disbursing claim...') From 4e02c05c2b516eb49cb3e3df5ed530c65b905aa1 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:48:30 -0600 Subject: [PATCH 37/66] remove redundant map --- dev-tools/compose/nginx_ingress.conf | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dev-tools/compose/nginx_ingress.conf b/dev-tools/compose/nginx_ingress.conf index 0600a804d7f..d7634859be7 100644 --- a/dev-tools/compose/nginx_ingress.conf +++ b/dev-tools/compose/nginx_ingress.conf @@ -252,10 +252,6 @@ server { # # ETH, POA, SOL # -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} server { listen 80; server_name audius-protocol-eth-ganache-1; From 11f6edacfce1a7a6401f0b8d1b3d5dfff27e67ba Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:54:42 -0600 Subject: [PATCH 38/66] fix identity solana relay test --- .../test/routes/solanaRelayTest.ts | 152 ++++++++++-------- packages/spl/src/index.ts | 1 + 2 files changed, 84 insertions(+), 69 deletions(-) diff --git a/packages/identity-service/test/routes/solanaRelayTest.ts b/packages/identity-service/test/routes/solanaRelayTest.ts index 3822dcb2634..a5f0843e8df 100644 --- a/packages/identity-service/test/routes/solanaRelayTest.ts +++ b/packages/identity-service/test/routes/solanaRelayTest.ts @@ -20,11 +20,11 @@ import { assertRelayAllowedInstructions } from '../../src/typed-routes/solana/so import config from '../../src/config' import audiusLibsWrapper from '../../src/audiusLibsInstance' import { InvalidRelayInstructionError } from '../../src/typed-routes/solana/InvalidRelayInstructionError' -import { createSenderPublicInstruction } from '@audius/spl/src/reward-manager/createSenderPublic' -import { RewardManagerInstruction } from '@audius/spl/src/reward-manager/constants' -import { createEvaluateAttestationsInstruction } from '@audius/spl/src/reward-manager/evaluateAttestations' -import { createSubmitAttestationInstruction } from '@audius/spl/src/reward-manager/submitAttestation' -import { ClaimableTokensProgram } from '@audius/spl' +import { + ClaimableTokensProgram, + RewardManagerProgram, + RewardManagerInstruction +} from '@audius/spl' const CLAIMABLE_TOKEN_PROGRAM_ID = new PublicKey( config.get('solanaClaimableTokenProgramAddress') @@ -289,77 +289,77 @@ describe('Solana Relay', function () { describe('Reward Manager Program', function () { it('should allow public instructions with valid reward manager', async function () { - const transferId = 'some:id:thing' + const disbursementId = 'some:id:thing' // Some dummy eth addresses to make the encoder happy const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' - const destinationEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' - const verifiedMessages = getRandomPublicKey() + const recipientEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' + const attestations = getRandomPublicKey() const authority = getRandomPublicKey() const payer = getRandomPublicKey() const sender = getRandomPublicKey() const rewardManagerTokenSource = getRandomPublicKey() - const destinationUserbank = getRandomPublicKey() - const transferAccount = getRandomPublicKey() - const antiAbuse = getRandomPublicKey() + const destinationUserBank = getRandomPublicKey() + const disbursementAccount = getRandomPublicKey() + const antiAbuseOracle = getRandomPublicKey() const existingSenders = [ getRandomPublicKey(), getRandomPublicKey(), getRandomPublicKey() ] await assertRelayAllowedInstructions([ - createSenderPublicInstruction( + RewardManagerProgram.createSenderPublicInstruction({ senderEthAddress, operatorEthAddress, - REWARD_MANAGER_ACCOUNT, + rewardManagerState: REWARD_MANAGER_ACCOUNT, authority, payer, sender, existingSenders, - REWARD_MANAGER_PROGRAM_ID - ), - createSubmitAttestationInstruction( - transferId, - verifiedMessages, - REWARD_MANAGER_ACCOUNT, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }), + RewardManagerProgram.createSubmitAttestationInstruction({ + disbursementId, + attestations, + rewardManagerState: REWARD_MANAGER_ACCOUNT, authority, payer, sender, - REWARD_MANAGER_PROGRAM_ID - ), - createEvaluateAttestationsInstruction( - transferId, - destinationEthAddress, - 100n, - verifiedMessages, - REWARD_MANAGER_ACCOUNT, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }), + RewardManagerProgram.createEvaluateAttestationsInstruction({ + disbursementId, + recipientEthAddress, + amount: 100n, + attestations, + rewardManagerState: REWARD_MANAGER_ACCOUNT, authority, rewardManagerTokenSource, - destinationUserbank, - transferAccount, - antiAbuse, + destinationUserBank, + disbursementAccount, + antiAbuseOracle, payer, - TOKEN_PROGRAM_ID, - REWARD_MANAGER_PROGRAM_ID - ) + tokenProgramId: TOKEN_PROGRAM_ID, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]) }) it('should not allow public instructions with invalid reward manager', async function () { - const transferId = 'some:id:thing' + const disbursementId = 'some:id:thing' // Some dummy eth addresses to make the encoder happy const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' - const destinationEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' - const verifiedMessages = getRandomPublicKey() + const recipientEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' + const attestations = getRandomPublicKey() const authority = getRandomPublicKey() const payer = getRandomPublicKey() const sender = getRandomPublicKey() - const rewardManager = getRandomPublicKey() + const rewardManagerState = getRandomPublicKey() const rewardManagerTokenSource = getRandomPublicKey() - const destinationUserbank = getRandomPublicKey() - const transferAccount = getRandomPublicKey() - const antiAbuse = getRandomPublicKey() + const destinationUserBank = getRandomPublicKey() + const disbursementAccount = getRandomPublicKey() + const antiAbuseOracle = getRandomPublicKey() const existingSenders = [ getRandomPublicKey(), getRandomPublicKey(), @@ -368,16 +368,16 @@ describe('Solana Relay', function () { await assert.rejects( async () => assertRelayAllowedInstructions([ - createSenderPublicInstruction( + RewardManagerProgram.createSenderPublicInstruction({ senderEthAddress, operatorEthAddress, - rewardManager, + rewardManagerState, authority, payer, sender, existingSenders, - REWARD_MANAGER_PROGRAM_ID - ) + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]), InvalidRelayInstructionError, 'invalid reward manager for createSenderPublic' @@ -386,15 +386,15 @@ describe('Solana Relay', function () { await assert.rejects( async () => assertRelayAllowedInstructions([ - createSubmitAttestationInstruction( - transferId, - verifiedMessages, - rewardManager, + RewardManagerProgram.createSubmitAttestationInstruction({ + disbursementId, + attestations, + rewardManagerState, authority, payer, sender, - REWARD_MANAGER_PROGRAM_ID - ) + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]), InvalidRelayInstructionError, 'invalid reward manager for submitAttestation' @@ -402,21 +402,21 @@ describe('Solana Relay', function () { await assert.rejects( async () => assertRelayAllowedInstructions([ - createEvaluateAttestationsInstruction( - transferId, - destinationEthAddress, - 100n, - verifiedMessages, - rewardManager, + RewardManagerProgram.createEvaluateAttestationsInstruction({ + disbursementId, + recipientEthAddress, + amount: 100n, + attestations, + rewardManagerState, authority, rewardManagerTokenSource, - destinationUserbank, - transferAccount, - antiAbuse, + destinationUserBank, + disbursementAccount, + antiAbuseOracle, payer, - TOKEN_PROGRAM_ID, - REWARD_MANAGER_PROGRAM_ID - ) + tokenProgramId: TOKEN_PROGRAM_ID, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]), InvalidRelayInstructionError, 'invalid reward manager for evaluateAttestations' @@ -424,16 +424,30 @@ describe('Solana Relay', function () { }) it('should not allow non-public instructions', async function () { + const disbursementId = 'some:id:thing' + // Some dummy eth addresses to make the encoder happy + const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' + const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' + const authority = getRandomPublicKey() + const payer = getRandomPublicKey() + const sender = getRandomPublicKey() + const rewardManagerState = getRandomPublicKey() + const manager = getRandomPublicKey() await assert.rejects( async () => assertRelayAllowedInstructions([ - new TransactionInstruction({ - programId: REWARD_MANAGER_PROGRAM_ID, - keys: [], - data: Buffer.from([RewardManagerInstruction.Init]) + RewardManagerProgram.createSenderInstruction({ + senderEthAddress, + operatorEthAddress, + rewardManagerState, + manager, + authority, + payer, + sender, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID }) ]), - 'reward manager init' + 'reward manager create sender' ) await assert.rejects( async () => @@ -452,10 +466,10 @@ describe('Solana Relay', function () { new TransactionInstruction({ programId: REWARD_MANAGER_PROGRAM_ID, keys: [], - data: Buffer.from([RewardManagerInstruction.CreateSender]) + data: Buffer.from([RewardManagerInstruction.Init]) }) ]), - 'non public create sender' + 'reward manager init' ) await assert.rejects( async () => diff --git a/packages/spl/src/index.ts b/packages/spl/src/index.ts index e5dc0f15031..636bc48d52b 100644 --- a/packages/spl/src/index.ts +++ b/packages/spl/src/index.ts @@ -1,4 +1,5 @@ export { ClaimableTokensProgram } from './claimable-tokens/ClaimableTokensProgram' +export { RewardManagerInstruction } from './reward-manager/constants' export { RewardManagerProgram } from './reward-manager/RewardManagerProgram' export { ethAddress } from './layout-utils' export * from './associated-token' From 68ab9b8209a10dfb4b6f18aa14c7b841aadb22e6 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 28 Dec 2023 22:18:41 -0600 Subject: [PATCH 39/66] fix solana relay tests --- .../assertRelayAllowedInstructions.test.ts | 144 ++++++++++-------- .../test/routes/solanaRelayTest.ts | 1 - 2 files changed, 78 insertions(+), 67 deletions(-) diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts index 47d37209e20..9051b8f13f8 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts @@ -20,9 +20,7 @@ import { assertRelayAllowedInstructions } from './assertRelayAllowedInstructions import { config } from '../../config' import { ClaimableTokensProgram, - createEvaluateAttestationsInstruction, - createSenderPublicInstruction, - createSubmitAttestationInstruction, + RewardManagerProgram, RewardManagerInstruction } from '@audius/spl' import { InvalidRelayInstructionError } from './InvalidRelayInstructionError' @@ -68,7 +66,8 @@ const getInittedLibs = async () => { rewardsManagerProgramPDA: config.rewardsManagerAccountAddress, rewardsManagerTokenPDA: '', useRelay: false, - confirmationTimeout: 0 + confirmationTimeout: 0, + paymentRouterProgramId: config.paymentRouterProgramId } }) await libs.init() @@ -307,77 +306,77 @@ describe('Solana Relay', function () { describe('Reward Manager Program', function () { it('should allow public instructions with valid reward manager', async function () { - const transferId = 'some:id:thing' + const disbursementId = 'some:id:thing' // Some dummy eth addresses to make the encoder happy const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' - const destinationEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' - const verifiedMessages = getRandomPublicKey() + const recipientEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' + const attestations = getRandomPublicKey() const authority = getRandomPublicKey() const payer = getRandomPublicKey() const sender = getRandomPublicKey() const rewardManagerTokenSource = getRandomPublicKey() - const destinationUserbank = getRandomPublicKey() - const transferAccount = getRandomPublicKey() - const antiAbuse = getRandomPublicKey() + const destinationUserBank = getRandomPublicKey() + const disbursementAccount = getRandomPublicKey() + const antiAbuseOracle = getRandomPublicKey() const existingSenders = [ getRandomPublicKey(), getRandomPublicKey(), getRandomPublicKey() ] await assertRelayAllowedInstructions([ - createSenderPublicInstruction( + RewardManagerProgram.createSenderPublicInstruction({ senderEthAddress, operatorEthAddress, - REWARD_MANAGER_ACCOUNT, + rewardManagerState: REWARD_MANAGER_ACCOUNT, authority, payer, sender, existingSenders, - REWARD_MANAGER_PROGRAM_ID - ), - createSubmitAttestationInstruction( - transferId, - verifiedMessages, - REWARD_MANAGER_ACCOUNT, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }), + RewardManagerProgram.createSubmitAttestationInstruction({ + disbursementId, + attestations, + rewardManagerState: REWARD_MANAGER_ACCOUNT, authority, payer, sender, - REWARD_MANAGER_PROGRAM_ID - ), - createEvaluateAttestationsInstruction( - transferId, - destinationEthAddress, - BigInt(100), - verifiedMessages, - REWARD_MANAGER_ACCOUNT, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }), + RewardManagerProgram.createEvaluateAttestationsInstruction({ + disbursementId, + recipientEthAddress, + amount: BigInt(100), + attestations, + rewardManagerState: REWARD_MANAGER_ACCOUNT, authority, rewardManagerTokenSource, - destinationUserbank, - transferAccount, - antiAbuse, + destinationUserBank, + disbursementAccount, + antiAbuseOracle, payer, - TOKEN_PROGRAM_ID, - REWARD_MANAGER_PROGRAM_ID - ) + tokenProgramId: TOKEN_PROGRAM_ID, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]) }) it('should not allow public instructions with invalid reward manager', async function () { - const transferId = 'some:id:thing' + const disbursementId = 'some:id:thing' // Some dummy eth addresses to make the encoder happy const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' - const destinationEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' - const verifiedMessages = getRandomPublicKey() + const recipientEthAddress = '0x7311c8ec02f087cba0fdbb056d4cebc86519d871' + const attestations = getRandomPublicKey() const authority = getRandomPublicKey() const payer = getRandomPublicKey() const sender = getRandomPublicKey() - const rewardManager = getRandomPublicKey() + const rewardManagerState = getRandomPublicKey() const rewardManagerTokenSource = getRandomPublicKey() - const destinationUserbank = getRandomPublicKey() - const transferAccount = getRandomPublicKey() - const antiAbuse = getRandomPublicKey() + const destinationUserBank = getRandomPublicKey() + const disbursementAccount = getRandomPublicKey() + const antiAbuseOracle = getRandomPublicKey() const existingSenders = [ getRandomPublicKey(), getRandomPublicKey(), @@ -386,16 +385,16 @@ describe('Solana Relay', function () { await assert.rejects( async () => assertRelayAllowedInstructions([ - createSenderPublicInstruction( + RewardManagerProgram.createSenderPublicInstruction({ senderEthAddress, operatorEthAddress, - rewardManager, + rewardManagerState, authority, payer, sender, existingSenders, - REWARD_MANAGER_PROGRAM_ID - ) + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]), InvalidRelayInstructionError, 'invalid reward manager for createSenderPublic' @@ -404,15 +403,15 @@ describe('Solana Relay', function () { await assert.rejects( async () => assertRelayAllowedInstructions([ - createSubmitAttestationInstruction( - transferId, - verifiedMessages, - rewardManager, + RewardManagerProgram.createSubmitAttestationInstruction({ + disbursementId, + attestations, + rewardManagerState, authority, payer, sender, - REWARD_MANAGER_PROGRAM_ID - ) + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]), InvalidRelayInstructionError, 'invalid reward manager for submitAttestation' @@ -420,21 +419,21 @@ describe('Solana Relay', function () { await assert.rejects( async () => assertRelayAllowedInstructions([ - createEvaluateAttestationsInstruction( - transferId, - destinationEthAddress, - BigInt(100), - verifiedMessages, - rewardManager, + RewardManagerProgram.createEvaluateAttestationsInstruction({ + disbursementId, + recipientEthAddress, + amount: BigInt(100), + attestations, + rewardManagerState, authority, rewardManagerTokenSource, - destinationUserbank, - transferAccount, - antiAbuse, + destinationUserBank, + disbursementAccount, + antiAbuseOracle, payer, - TOKEN_PROGRAM_ID, - REWARD_MANAGER_PROGRAM_ID - ) + tokenProgramId: TOKEN_PROGRAM_ID, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID + }) ]), InvalidRelayInstructionError, 'invalid reward manager for evaluateAttestations' @@ -464,16 +463,29 @@ describe('Solana Relay', function () { ]), 'reward manager change manager account' ) + // Some dummy eth addresses to make the encoder happy + const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' + const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' + const authority = getRandomPublicKey() + const payer = getRandomPublicKey() + const sender = getRandomPublicKey() + const rewardManagerState = getRandomPublicKey() + const manager = getRandomPublicKey() await assert.rejects( async () => assertRelayAllowedInstructions([ - new TransactionInstruction({ - programId: REWARD_MANAGER_PROGRAM_ID, - keys: [], - data: Buffer.from([RewardManagerInstruction.CreateSender]) + RewardManagerProgram.createSenderInstruction({ + senderEthAddress, + operatorEthAddress, + rewardManagerState, + manager, + authority, + payer, + sender, + rewardManagerProgramId: REWARD_MANAGER_PROGRAM_ID }) ]), - 'non public create sender' + 'reward manager create sender' ) await assert.rejects( async () => diff --git a/packages/identity-service/test/routes/solanaRelayTest.ts b/packages/identity-service/test/routes/solanaRelayTest.ts index a5f0843e8df..53df3fd8495 100644 --- a/packages/identity-service/test/routes/solanaRelayTest.ts +++ b/packages/identity-service/test/routes/solanaRelayTest.ts @@ -424,7 +424,6 @@ describe('Solana Relay', function () { }) it('should not allow non-public instructions', async function () { - const disbursementId = 'some:id:thing' // Some dummy eth addresses to make the encoder happy const senderEthAddress = '0x1dc3070311552fce47e06db9f4f1328187f14c85' const operatorEthAddress = '0x430ef095e4c5ac71a465b30d566bab0bb0985346' From fc2f8207a5cc1d46c96fea513b6358823ad5c7b1 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:34:28 -0600 Subject: [PATCH 40/66] trial-and-error the ci bug --- packages/spl/tsconfig.base.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/spl/tsconfig.base.json b/packages/spl/tsconfig.base.json index baeb8a34d98..3bcef813a13 100644 --- a/packages/spl/tsconfig.base.json +++ b/packages/spl/tsconfig.base.json @@ -9,6 +9,7 @@ "noEmitOnError": true, "resolveJsonModule": true, "strict": true, - "stripInternal": true + "stripInternal": true, + "skipLibCheck": true } } From b9e96de47203924cd69e29569ba1123052b700fb Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:15:27 -0600 Subject: [PATCH 41/66] Rename get_all_other_nodes => get_all_nodes and restore backwards compatibility --- .../confirm_indexing_transaction_error.py | 6 +-- .../src/queries/get_attestation.py | 7 ++-- .../src/queries/get_health.py | 10 ++++- .../src/queries/get_redirect_weights.py | 5 ++- .../src/tasks/cache_current_nodes.py | 37 +++++++------------ .../src/tasks/index_metrics.py | 16 ++++---- .../src/tasks/update_clique_signers.py | 5 ++- .../src/utils/get_all_other_nodes.py | 17 ++------- .../DiscoveryNodeSelector.ts | 7 +++- .../DiscoveryNodeSelector/healthCheckTypes.ts | 3 +- 10 files changed, 55 insertions(+), 58 deletions(-) diff --git a/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py b/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py index 76cc3e3145e..1927242dfff 100644 --- a/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py +++ b/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py @@ -3,7 +3,7 @@ import requests from src.queries.get_skipped_transactions import set_indexing_error -from src.utils.get_all_other_nodes import get_all_other_discovery_nodes_cached +from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached logger = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def confirm_indexing_transaction_error( Gets all other discovery nodes and makes an api call to check the status of a transaction given a blocknumber, blockhash, and transactionhash """ - all_other_nodes = get_all_other_discovery_nodes_cached(redis) + all_other_nodes = get_all_discovery_nodes_cached(redis) or [] if not all_other_nodes: return False @@ -27,7 +27,7 @@ def confirm_indexing_transaction_error( num_transaction_failures = 0 for node in all_other_nodes: try: - endpoint = f"{node}/indexing/transaction_status?blocknumber={blocknumber}&blockhash={blockhash}&transactionhash={transactionhash}" + endpoint = f"{node['endpoint']}/indexing/transaction_status?blocknumber={blocknumber}&blockhash={blockhash}&transactionhash={transactionhash}" response = requests.get(endpoint, timeout=10) if response.status_code != 200: raise Exception( diff --git a/packages/discovery-provider/src/queries/get_attestation.py b/packages/discovery-provider/src/queries/get_attestation.py index bb75fa64ceb..c53bb947bc7 100644 --- a/packages/discovery-provider/src/queries/get_attestation.py +++ b/packages/discovery-provider/src/queries/get_attestation.py @@ -24,7 +24,7 @@ oracle_addresses_key, ) from src.utils.config import shared_config -from src.utils.get_all_other_nodes import get_all_other_discovery_nodes_wallets_cached +from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached from src.utils.redis_connection import get_redis REWARDS_MANAGER_ACCOUNT = shared_config["solana"]["rewards_manager_account"] @@ -214,8 +214,9 @@ def get_attestation( def verify_discovery_node_exists_on_chain(new_sender_address: str) -> bool: redis = get_redis() - other_nodes_addresses = set(get_all_other_discovery_nodes_wallets_cached(redis)) - return new_sender_address in other_nodes_addresses + nodes = get_all_discovery_nodes_cached(redis) + wallets = set([d["delegateOwnerWallet"] for d in nodes] if nodes else []) + return new_sender_address in wallets def get_create_sender_attestation(new_sender_address: str) -> Tuple[str, str]: diff --git a/packages/discovery-provider/src/queries/get_health.py b/packages/discovery-provider/src/queries/get_health.py index 926a5f7f7b8..32108267b7a 100644 --- a/packages/discovery-provider/src/queries/get_health.py +++ b/packages/discovery-provider/src/queries/get_health.py @@ -322,7 +322,7 @@ def get_health(args: GetHealthArgs, use_redis_cache: bool = True) -> Tuple[Dict, ) == "postgresql://postgres:postgres@db:5432/audius_discovery" or "localhost" in os.getenv( "audius_db_url", "" ) - discovery_nodes = get_all_other_nodes.get_all_other_discovery_nodes_cached(redis) + discovery_nodes = get_all_other_nodes.get_all_discovery_nodes_cached(redis) content_nodes = get_all_other_nodes.get_all_healthy_content_nodes_cached(redis) final_poa_block = helpers.get_final_poa_block() health_results = { @@ -359,7 +359,13 @@ def get_health(args: GetHealthArgs, use_redis_cache: bool = True) -> Tuple[Dict, "latest_block_num": latest_block_num, "latest_indexed_block_num": latest_indexed_block_num, "final_poa_block": final_poa_block, - "network": {"discovery_nodes": discovery_nodes, "content_nodes": content_nodes}, + "network": { + "discovery_nodes_with_owner": discovery_nodes, + "discovery_nodes": [d["endpoint"] for d in discovery_nodes] + if discovery_nodes + else None, + "content_nodes": content_nodes, + }, } if os.getenv("AUDIUS_DOCKER_COMPOSE_GIT_SHA") is not None: diff --git a/packages/discovery-provider/src/queries/get_redirect_weights.py b/packages/discovery-provider/src/queries/get_redirect_weights.py index eaf1834f7c7..9d0d3431c05 100644 --- a/packages/discovery-provider/src/queries/get_redirect_weights.py +++ b/packages/discovery-provider/src/queries/get_redirect_weights.py @@ -4,7 +4,7 @@ from flask import Blueprint from src.api_helpers import success_response -from src.utils.get_all_other_nodes import get_all_other_discovery_nodes_cached +from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached from src.utils.redis_cache import cache, internal_api_cache_prefix from src.utils.redis_connection import get_redis @@ -17,7 +17,8 @@ @cache(ttl_sec=10 * 60, cache_prefix_override=internal_api_cache_prefix) def redirect_weights(): redis = get_redis() - endpoints = get_all_other_discovery_nodes_cached(redis) + nodes = get_all_discovery_nodes_cached(redis) + endpoints = [d["endpoint"] for d in nodes] if nodes else [] loads = {} for endpoint in endpoints: try: diff --git a/packages/discovery-provider/src/tasks/cache_current_nodes.py b/packages/discovery-provider/src/tasks/cache_current_nodes.py index 30f67af5003..48c4d91c155 100644 --- a/packages/discovery-provider/src/tasks/cache_current_nodes.py +++ b/packages/discovery-provider/src/tasks/cache_current_nodes.py @@ -4,11 +4,10 @@ from src.utils.get_all_other_nodes import ( ALL_CONTENT_NODES_CACHE_KEY, ALL_DISCOVERY_NODES_CACHE_KEY, - ALL_DISCOVERY_NODES_WALLETS_CACHE_KEY, ALL_HEALTHY_CONTENT_NODES_CACHE_KEY, filter_healthy_content_nodes, - get_all_other_content_nodes, - get_all_other_discovery_nodes, + get_all_content_nodes, + get_all_discovery_nodes, get_node_endpoint, ) from src.utils.prometheus_metric import save_duration_metric @@ -33,29 +32,21 @@ def cache_current_nodes_task(self): have_lock = update_lock.acquire(blocking=False) if have_lock: logger.info("cache_current_nodes.py | fetching all other discovery nodes") - discovery_nodes, discovery_nodes_wallets = get_all_other_discovery_nodes() - current_node = get_node_endpoint() - # add current node to list - if current_node is not None: - discovery_nodes.append(current_node) - - set_json_cached_key( - redis, - ALL_DISCOVERY_NODES_CACHE_KEY, - [ - {"endpoint": endpoint, "delegateOwnerWallet": delegateOwnerWallet} - for endpoint, delegateOwnerWallet in zip( - discovery_nodes, discovery_nodes_wallets - ) - ], - ) - set_json_cached_key( - redis, ALL_DISCOVERY_NODES_WALLETS_CACHE_KEY, discovery_nodes_wallets - ) + ( + discovery_node_endpoints, + discovery_nodes_wallets, + ) = get_all_discovery_nodes() + discovery_nodes = [ + {"endpoint": endpoint, "delegateOwnerWallet": delegateOwnerWallet} + for endpoint, delegateOwnerWallet in zip( + discovery_node_endpoints, discovery_nodes_wallets + ) + ] + set_json_cached_key(redis, ALL_DISCOVERY_NODES_CACHE_KEY, discovery_nodes) logger.info("cache_current_nodes.py | set current discovery nodes in redis") logger.info("cache_current_nodes.py | fetching all other content nodes") - content_node_endpoints, content_node_wallets = get_all_other_content_nodes() + content_node_endpoints, content_node_wallets = get_all_content_nodes() content_nodes = [ {"endpoint": endpoint, "delegateOwnerWallet": delegateOwnerWallet} for endpoint, delegateOwnerWallet in zip( diff --git a/packages/discovery-provider/src/tasks/index_metrics.py b/packages/discovery-provider/src/tasks/index_metrics.py index e215bdb3ac1..2cd9b5efd77 100644 --- a/packages/discovery-provider/src/tasks/index_metrics.py +++ b/packages/discovery-provider/src/tasks/index_metrics.py @@ -11,7 +11,7 @@ update_historical_monthly_route_metrics, ) from src.tasks.celery_app import celery -from src.utils.get_all_other_nodes import get_all_other_discovery_nodes_cached +from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached from src.utils.prometheus_metric import ( PrometheusMetric, PrometheusMetricNames, @@ -76,7 +76,7 @@ def consolidate_metrics_from_other_nodes(self, db, redis): and merge with this node's metrics so that this node will be aware of all the metrics across users hitting different providers """ - all_other_nodes = get_all_other_discovery_nodes_cached(redis) + all_other_nodes = get_all_discovery_nodes_cached(redis) or [] visited_node_timestamps_str = redis.get(metrics_visited_nodes) visited_node_timestamps = ( @@ -128,8 +128,8 @@ def consolidate_metrics_from_other_nodes(self, db, redis): # Merge & persist metrics for other nodes for node in all_other_nodes: start_time_str = ( - visited_node_timestamps[node] - if node in visited_node_timestamps + visited_node_timestamps[node["endpoint"]] + if node["endpoint"] in visited_node_timestamps else one_iteration_ago_str ) start_time_obj = datetime.strptime(start_time_str, datetime_format_secondary) @@ -237,10 +237,12 @@ def synchronize_all_node_metrics(self, db, redis): monthly_route_metrics = {} daily_app_metrics = {} monthly_app_metrics = {} - all_other_nodes = get_all_other_discovery_nodes_cached(redis) + all_other_nodes = get_all_discovery_nodes_cached(redis) or [] for node in all_other_nodes: - historical_metrics = get_historical_metrics(node) - logger.debug(f"got historical metrics from {node}: {historical_metrics}") + historical_metrics = get_historical_metrics(node["endpoint"]) + logger.debug( + f"got historical metrics from {node['endpoint']}: {historical_metrics}" + ) if historical_metrics: update_route_metrics_count( daily_route_metrics, historical_metrics["routes"]["daily"] diff --git a/packages/discovery-provider/src/tasks/update_clique_signers.py b/packages/discovery-provider/src/tasks/update_clique_signers.py index cde95c1aec5..1ff28b5a2a8 100644 --- a/packages/discovery-provider/src/tasks/update_clique_signers.py +++ b/packages/discovery-provider/src/tasks/update_clique_signers.py @@ -3,7 +3,7 @@ import requests from src.tasks.celery_app import celery -from src.utils.get_all_other_nodes import get_all_other_discovery_nodes_cached +from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached from src.utils.prometheus_metric import save_duration_metric logger = logging.getLogger(__name__) @@ -31,8 +31,9 @@ def update_signers(self, redis): # the maximum signers in addition to the registered static nodes max_signers = int(shared_config["discprov"]["max_signers"]) + nodes = get_all_discovery_nodes_cached(redis) other_wallets = set( - [wallet.lower() for wallet in get_all_other_discovery_nodes_cached(redis)[1]] + [node["delegateOwnerWallet"].lower() for node in nodes] if nodes else None ) logger.info( f"update_clique_signers.py | Other registered discovery addresses: {other_wallets}" diff --git a/packages/discovery-provider/src/utils/get_all_other_nodes.py b/packages/discovery-provider/src/utils/get_all_other_nodes.py index 43a266af54e..d1c6e76d676 100644 --- a/packages/discovery-provider/src/utils/get_all_other_nodes.py +++ b/packages/discovery-provider/src/utils/get_all_other_nodes.py @@ -18,7 +18,6 @@ DISCOVERY_NODE_SERVICE_TYPE = bytes("discovery-node", "utf-8") CONTENT_NODE_SERVICE_TYPE = "content-node".encode("utf-8") ALL_DISCOVERY_NODES_CACHE_KEY = "all-discovery-nodes" -ALL_DISCOVERY_NODES_WALLETS_CACHE_KEY = "all-discovery-nodes-wallets" ALL_CONTENT_NODES_CACHE_KEY = "all-content-nodes" ALL_HEALTHY_CONTENT_NODES_CACHE_KEY = "all-healthy-content-nodes" @@ -103,11 +102,11 @@ async def fetch_results(): return all_other_nodes, all_other_wallets -def get_all_other_discovery_nodes() -> Tuple[List[str], List[str]]: +def get_all_discovery_nodes() -> Tuple[List[str], List[str]]: return get_all_nodes(DISCOVERY_NODE_SERVICE_TYPE) -def get_all_other_content_nodes() -> Tuple[List[str], List[str]]: +def get_all_content_nodes() -> Tuple[List[str], List[str]]: return get_all_nodes(CONTENT_NODE_SERVICE_TYPE) @@ -121,7 +120,7 @@ def filter_healthy_content_nodes(all_content_nodes: List[Dict[str, str]]): return [node for node in healthy_nodes if node is not None] -def get_all_other_discovery_nodes_cached(redis) -> List[str]: +def get_all_discovery_nodes_cached(redis) -> List[Dict[str, str]]: """ Attempts to get the enumerated discovery nodes from redis. """ @@ -129,15 +128,7 @@ def get_all_other_discovery_nodes_cached(redis) -> List[str]: return get_json_cached_key(redis, ALL_DISCOVERY_NODES_CACHE_KEY) -def get_all_other_discovery_nodes_wallets_cached(redis) -> List[str]: - """ - Attempts to get the enumerated discovery node wallet addresses from redis. - """ - - return get_json_cached_key(redis, ALL_DISCOVERY_NODES_WALLETS_CACHE_KEY) - - -def get_all_other_content_nodes_cached(redis) -> List[Dict[str, str]]: +def get_all_content_nodes_cached(redis) -> List[Dict[str, str]]: """ Attempts to get the content nodes from redis. """ diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index d3f54f9ad98..d909bc61967 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -602,7 +602,10 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { // Cancel any existing requests from other promises abortController.abort() // Refresh service list with the healthy list from DN - await this.refreshServiceList(endpoint, data?.network?.discovery_nodes) + await this.refreshServiceList( + endpoint, + data?.network?.discovery_nodes_with_owner + ) return endpoint } }) @@ -621,7 +624,7 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { endpoint, healthCheckThresholds: this.config.healthCheckThresholds }) - nodes = data?.network?.discovery_nodes + nodes = data?.network?.discovery_nodes_with_owner } if (nodes && nodes.length > 0) { this.logger.debug(`Refreshed service list with ${nodes.length} nodes.`) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts index 99ca7d70644..76a8d57491b 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts @@ -58,7 +58,8 @@ export type HealthCheckResponseData = DeepPartial<{ meets_min_requirements: boolean network: { content_nodes: StorageNode[] - discovery_nodes: DiscoveryNode[] + discovery_nodes: string[] + discovery_nodes_with_owner: DiscoveryNode[] } num_users_in_immediate_balance_refresh_queue: number num_users_in_lazy_balance_refresh_queue: number From 3c81bd16452e7074c94daaeb4b06a8129ba53810 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 29 Dec 2023 16:13:32 -0600 Subject: [PATCH 42/66] Remove get_node_endpoint --- .../src/tasks/cache_current_nodes.py | 1 - .../src/utils/get_all_other_nodes.py | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/packages/discovery-provider/src/tasks/cache_current_nodes.py b/packages/discovery-provider/src/tasks/cache_current_nodes.py index 48c4d91c155..db428591fee 100644 --- a/packages/discovery-provider/src/tasks/cache_current_nodes.py +++ b/packages/discovery-provider/src/tasks/cache_current_nodes.py @@ -8,7 +8,6 @@ filter_healthy_content_nodes, get_all_content_nodes, get_all_discovery_nodes, - get_node_endpoint, ) from src.utils.prometheus_metric import save_duration_metric from src.utils.redis_cache import set_json_cached_key diff --git a/packages/discovery-provider/src/utils/get_all_other_nodes.py b/packages/discovery-provider/src/utils/get_all_other_nodes.py index d1c6e76d676..4ff09e2f61c 100644 --- a/packages/discovery-provider/src/utils/get_all_other_nodes.py +++ b/packages/discovery-provider/src/utils/get_all_other_nodes.py @@ -45,16 +45,6 @@ def fetch_node_info(sp_id, sp_factory_instance, service_type): ).call() -def get_node_endpoint() -> Optional[str]: - """ - Get endpoint for this discovery node from the config - """ - node_url = shared_config["discprov"]["url"] - if not node_url or not is_fqdn(node_url): - return None - return node_url.rstrip("/") - - async def get_async_node(sp_id, sp_factory_instance, service_type): loop = asyncio.get_running_loop() result = await loop.run_in_executor( From 85834c1f5108ea54e2c9fc7ae695e6713f12c9df Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Mon, 1 Jan 2024 22:53:52 -0600 Subject: [PATCH 43/66] remove unused --- packages/discovery-provider/src/utils/get_all_other_nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discovery-provider/src/utils/get_all_other_nodes.py b/packages/discovery-provider/src/utils/get_all_other_nodes.py index 4ff09e2f61c..1e40a3f9fa0 100644 --- a/packages/discovery-provider/src/utils/get_all_other_nodes.py +++ b/packages/discovery-provider/src/utils/get_all_other_nodes.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import aiohttp from web3 import Web3 From d092eacfc208afdb945c27e3ee22668c0314b3ee Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:08:52 -0600 Subject: [PATCH 44/66] Fix test --- .../tasks/entity_manager/test_index_skip_tx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py index c26fd464238..3835bdda576 100644 --- a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py +++ b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py @@ -31,7 +31,7 @@ def get_events_side_effect(_, tx_receipt): ) mocker.patch( - "src.queries.confirm_indexing_transaction_error.get_all_other_discovery_nodes_cached", + "src.queries.confirm_indexing_transaction_error.get_all_discovery_nodes_cached", return_value=["node1", "node2", "node3"], autospec=True, ) From 9c133b2949d57a7d5ea8b9e7250ff2485e77397c Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:18:48 -0600 Subject: [PATCH 45/66] Fix mock for get_discovery_nodes --- .../tasks/entity_manager/test_index_skip_tx.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py index 3835bdda576..1d65e3cf41d 100644 --- a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py +++ b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py @@ -32,7 +32,11 @@ def get_events_side_effect(_, tx_receipt): mocker.patch( "src.queries.confirm_indexing_transaction_error.get_all_discovery_nodes_cached", - return_value=["node1", "node2", "node3"], + return_value=[ + {"endpoint": "node1"}, + {"endpoint": "node2"}, + {"endpoint": "node3"}, + ], autospec=True, ) From fe439bb88f8da717fca6ac436fea0ded3c0922e4 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:21:08 -0600 Subject: [PATCH 46/66] Fix mock for get_discovery_nodes --- .../integration_tests/queries/test_get_attestation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_attestation.py b/packages/discovery-provider/integration_tests/queries/test_get_attestation.py index 16124ecfe42..db95526b5dd 100644 --- a/packages/discovery-provider/integration_tests/queries/test_get_attestation.py +++ b/packages/discovery-provider/integration_tests/queries/test_get_attestation.py @@ -276,8 +276,10 @@ def test_get_attestation_weekly_pool_exhausted(app): @pytest.fixture def patch_get_all_other_nodes(): with patch( - "src.queries.get_attestation.get_all_other_discovery_nodes_wallets_cached", - return_value=["0x94e140D27F3d5EE9EcA0109A71CcBa0109964DCa"], + "src.queries.get_attestation.get_all_discovery_nodes_cached", + return_value=[ + {"delegateOwnerWallet": "0x94e140D27F3d5EE9EcA0109A71CcBa0109964DCa"} + ], ): yield From 0ac946a5479bb53d5d12bc7eb46707d8c8922c97 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 3 Jan 2024 00:23:08 -0600 Subject: [PATCH 47/66] Add jsdoc, generateSpecifiers --- .../src/sdk/api/challenges/ChallengesApi.ts | 56 ++++++++++-- packages/libs/src/sdk/api/challenges/types.ts | 88 ++++++++++++++++++- 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 1382ed58ed5..01e9139c8d2 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -16,7 +16,13 @@ import { Configuration as ConfigurationFull } from '../generated/full' import type { UsersApi } from '../users/UsersApi' -import { ClaimRewardsRequest, ClaimRewardsSchema } from './types' +import { + ChallengeRewardID, + ClaimRewardsRequest, + ClaimRewardsSchema, + GenerateSpecifierRequest, + GenerateSpecifierSchema +} from './types' import type { PublicKey } from '@solana/web3.js' import { AntiAbuseOracleApi } from '../antiAbuseOracle/AntiAbuseOracleApi' import { toChecksumAddress } from 'ethereumjs-util' @@ -36,7 +42,47 @@ export class ChallengesApi extends BaseAPI { this.logger = logger.createPrefixedLogger('[challenges-api]') } - public async claimRewards(request: ClaimRewardsRequest) { + /** + * Formats the specifier for a claimable instance of a challenge. + * + * For most challenges, this is the user ID of the user that's claiming the + * the challenge reward. For challenges like referrals or $AUDIO matching, + * this includes more data like the referred user ID or purchased track ID + * to allow the challenge to be claimed multiple times, but only once per + * completed instance. + */ + public async generateSpecifier(request: GenerateSpecifierRequest) { + const args = await parseParams( + 'generateSpecifier', + GenerateSpecifierSchema + )(request) + switch (args.challengeId) { + case ChallengeRewardID.COMPLETE_PROFILE: + case ChallengeRewardID.CONNECT_VERIFIED_ACCOUNT: + case ChallengeRewardID.CREATE_FIRST_PLAYLIST: + case ChallengeRewardID.LISTEN_STREAK: + case ChallengeRewardID.MOBILE_INSTALL: + case ChallengeRewardID.SEND_FIRST_TIP: + case ChallengeRewardID.TRACK_UPLOADS: + return `${args.userId}` + case ChallengeRewardID.AUDIO_MATCHING_BUYER: + return `${args.sellerUserId}=>${args.trackId}` + case ChallengeRewardID.AUDIO_MATCHING_SELLER: + return `${args.buyerUserId}=>${args.trackId}` + case ChallengeRewardID.REFERRALS: + case ChallengeRewardID.VERIFIED_REFERRALS: + return `${args.userId}=>${args.userId}` + default: + throw new Error(`Unknown challenge ID: ${args.challengeId}`) + } + } + + /** + * Claims a reward on behalf of a user. + * + * @see {@link generateSpecifier} to create the specifier argument. + */ + public async claimReward(request: ClaimRewardsRequest) { const args = await parseParams('claimRewards', ClaimRewardsSchema)(request) const { challengeId, specifier, amount: inputAmount } = args const { userId } = request @@ -140,7 +186,7 @@ export class ChallengesApi extends BaseAPI { handle }: { antiAbuseOracle: AntiAbuseOracle - challengeId: string + challengeId: ChallengeRewardID specifier: string amount: bigint recipientEthAddress: string @@ -190,7 +236,7 @@ export class ChallengesApi extends BaseAPI { }: { userId: string antiAbuseOracleEthAddress: string - challengeId: string + challengeId: ChallengeRewardID specifier: string amount: bigint recipientEthAddress: string @@ -252,7 +298,7 @@ export class ChallengesApi extends BaseAPI { antiAbuseOracleEthAddress, amount }: { - challengeId: string + challengeId: ChallengeRewardID specifier: string recipientEthAddress: string destinationUserBank: PublicKey diff --git a/packages/libs/src/sdk/api/challenges/types.ts b/packages/libs/src/sdk/api/challenges/types.ts index b3dbd5f601e..8a47f22873e 100644 --- a/packages/libs/src/sdk/api/challenges/types.ts +++ b/packages/libs/src/sdk/api/challenges/types.ts @@ -1,11 +1,95 @@ import { z } from 'zod' import { HashId } from '../../types/HashId' +// @ts-ignore:next-line ignore the unused import, used in jsdoc +import type { ChallengesApi } from './ChallengesApi' + +export enum ChallengeRewardID { + TRACK_UPLOADS = 'track-upload', + REFERRALS = 'referrals', + VERIFIED_REFERRALS = 'ref-v', + REFERRED = 'referred', + MOBILE_INSTALL = 'mobile-install', + CONNECT_VERIFIED_ACCOUNT = 'connect-verified', + LISTEN_STREAK = 'listen-streak', + COMPLETE_PROFILE = 'profile-completion', + SEND_FIRST_TIP = 'send-first-tip', + CREATE_FIRST_PLAYLIST = 'first-playlist', + AUDIO_MATCHING_BUYER = 'b', + AUDIO_MATCHING_SELLER = 's', + TRENDING_TRACK = 'tt', + TRENDING_PLAYLIST = 'tp', + TRENDING_UNDERGROUND_TRACK = 'tut' +} + +const DefaultSpecifier = z.object({ + /** The challenge identifier. As in, the challenge "name." */ + challengeId: z.enum([ + ChallengeRewardID.COMPLETE_PROFILE, + ChallengeRewardID.CONNECT_VERIFIED_ACCOUNT, + ChallengeRewardID.CREATE_FIRST_PLAYLIST, + ChallengeRewardID.LISTEN_STREAK, + ChallengeRewardID.MOBILE_INSTALL, + ChallengeRewardID.REFERRED, + ChallengeRewardID.SEND_FIRST_TIP, + ChallengeRewardID.TRACK_UPLOADS + ]), + /** The user ID of the user completing the challenge. */ + userId: HashId +}) + +const ReferralSpecifier = z.object({ + /** The challenge identifier. As in, the challenge "name." */ + challengeId: z.enum([ + ChallengeRewardID.REFERRALS, + ChallengeRewardID.VERIFIED_REFERRALS + ]), + /** The user ID of the user that was referred. */ + referredUserId: HashId, + /** The user ID of the user completing the challenge. */ + userId: HashId +}) + +const TrackSellerSpecifier = z.object({ + /** The challenge identifier. As in, the challenge "name." */ + challengeId: z.enum([ChallengeRewardID.AUDIO_MATCHING_BUYER]), + /** The user ID of the owner of the track purchased. */ + sellerUserId: HashId, + /** The track ID that was purchased. */ + trackId: HashId +}) + +const TrackBuyerSpecifier = z.object({ + /** The challenge identifier. As in, the challenge "name." */ + challengeId: z.enum([ChallengeRewardID.AUDIO_MATCHING_SELLER]), + /** The user ID of the user that bought the track. */ + buyerUserId: HashId, + /** The track ID that was purchased. */ + trackId: HashId +}) + +export const GenerateSpecifierSchema = z.union([ + DefaultSpecifier, + ReferralSpecifier, + TrackSellerSpecifier, + TrackBuyerSpecifier +]) + +export type GenerateSpecifierRequest = z.input + export const ClaimRewardsSchema = z .object({ - challengeId: z.string(), + /** The challenge identifier. As in, the challenge "name." */ + challengeId: z.nativeEnum(ChallengeRewardID), + /** + * Identifier for the completed challenge instance. + * + * @see {@link ChallengesApi.generateSpecifier} + */ specifier: z.string(), - amount: z.number(), + /** Reward amount, in either wAUDIO (number) or wAUDIO Wei (bigint) */ + amount: z.union([z.bigint(), z.number()]), + /** The user ID of the claimee. */ userId: HashId }) .strict() From 5f505f4f39bc3dc68c8e5e88a1e6a41648d569b4 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 3 Jan 2024 00:28:41 -0600 Subject: [PATCH 48/66] export the things, rename --- .../src/sdk/api/challenges/ChallengesApi.ts | 30 +++++++++---------- packages/libs/src/sdk/api/challenges/types.ts | 29 ++++++++---------- packages/libs/src/sdk/index.ts | 2 ++ packages/libs/src/sdk/sdkBrowserDist.ts | 2 ++ 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 01e9139c8d2..ed45df757ef 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -17,7 +17,7 @@ import { } from '../generated/full' import type { UsersApi } from '../users/UsersApi' import { - ChallengeRewardID, + ChallengeId, ClaimRewardsRequest, ClaimRewardsSchema, GenerateSpecifierRequest, @@ -57,20 +57,20 @@ export class ChallengesApi extends BaseAPI { GenerateSpecifierSchema )(request) switch (args.challengeId) { - case ChallengeRewardID.COMPLETE_PROFILE: - case ChallengeRewardID.CONNECT_VERIFIED_ACCOUNT: - case ChallengeRewardID.CREATE_FIRST_PLAYLIST: - case ChallengeRewardID.LISTEN_STREAK: - case ChallengeRewardID.MOBILE_INSTALL: - case ChallengeRewardID.SEND_FIRST_TIP: - case ChallengeRewardID.TRACK_UPLOADS: + case ChallengeId.COMPLETE_PROFILE: + case ChallengeId.CONNECT_VERIFIED_ACCOUNT: + case ChallengeId.CREATE_FIRST_PLAYLIST: + case ChallengeId.LISTEN_STREAK: + case ChallengeId.MOBILE_INSTALL: + case ChallengeId.SEND_FIRST_TIP: + case ChallengeId.TRACK_UPLOADS: return `${args.userId}` - case ChallengeRewardID.AUDIO_MATCHING_BUYER: + case ChallengeId.AUDIO_MATCHING_BUYER: return `${args.sellerUserId}=>${args.trackId}` - case ChallengeRewardID.AUDIO_MATCHING_SELLER: + case ChallengeId.AUDIO_MATCHING_SELLER: return `${args.buyerUserId}=>${args.trackId}` - case ChallengeRewardID.REFERRALS: - case ChallengeRewardID.VERIFIED_REFERRALS: + case ChallengeId.REFERRALS: + case ChallengeId.VERIFIED_REFERRALS: return `${args.userId}=>${args.userId}` default: throw new Error(`Unknown challenge ID: ${args.challengeId}`) @@ -186,7 +186,7 @@ export class ChallengesApi extends BaseAPI { handle }: { antiAbuseOracle: AntiAbuseOracle - challengeId: ChallengeRewardID + challengeId: ChallengeId specifier: string amount: bigint recipientEthAddress: string @@ -236,7 +236,7 @@ export class ChallengesApi extends BaseAPI { }: { userId: string antiAbuseOracleEthAddress: string - challengeId: ChallengeRewardID + challengeId: ChallengeId specifier: string amount: bigint recipientEthAddress: string @@ -298,7 +298,7 @@ export class ChallengesApi extends BaseAPI { antiAbuseOracleEthAddress, amount }: { - challengeId: ChallengeRewardID + challengeId: ChallengeId specifier: string recipientEthAddress: string destinationUserBank: PublicKey diff --git a/packages/libs/src/sdk/api/challenges/types.ts b/packages/libs/src/sdk/api/challenges/types.ts index 8a47f22873e..4af10c3e1e9 100644 --- a/packages/libs/src/sdk/api/challenges/types.ts +++ b/packages/libs/src/sdk/api/challenges/types.ts @@ -4,7 +4,7 @@ import { HashId } from '../../types/HashId' // @ts-ignore:next-line ignore the unused import, used in jsdoc import type { ChallengesApi } from './ChallengesApi' -export enum ChallengeRewardID { +export enum ChallengeId { TRACK_UPLOADS = 'track-upload', REFERRALS = 'referrals', VERIFIED_REFERRALS = 'ref-v', @@ -25,14 +25,14 @@ export enum ChallengeRewardID { const DefaultSpecifier = z.object({ /** The challenge identifier. As in, the challenge "name." */ challengeId: z.enum([ - ChallengeRewardID.COMPLETE_PROFILE, - ChallengeRewardID.CONNECT_VERIFIED_ACCOUNT, - ChallengeRewardID.CREATE_FIRST_PLAYLIST, - ChallengeRewardID.LISTEN_STREAK, - ChallengeRewardID.MOBILE_INSTALL, - ChallengeRewardID.REFERRED, - ChallengeRewardID.SEND_FIRST_TIP, - ChallengeRewardID.TRACK_UPLOADS + ChallengeId.COMPLETE_PROFILE, + ChallengeId.CONNECT_VERIFIED_ACCOUNT, + ChallengeId.CREATE_FIRST_PLAYLIST, + ChallengeId.LISTEN_STREAK, + ChallengeId.MOBILE_INSTALL, + ChallengeId.REFERRED, + ChallengeId.SEND_FIRST_TIP, + ChallengeId.TRACK_UPLOADS ]), /** The user ID of the user completing the challenge. */ userId: HashId @@ -40,10 +40,7 @@ const DefaultSpecifier = z.object({ const ReferralSpecifier = z.object({ /** The challenge identifier. As in, the challenge "name." */ - challengeId: z.enum([ - ChallengeRewardID.REFERRALS, - ChallengeRewardID.VERIFIED_REFERRALS - ]), + challengeId: z.enum([ChallengeId.REFERRALS, ChallengeId.VERIFIED_REFERRALS]), /** The user ID of the user that was referred. */ referredUserId: HashId, /** The user ID of the user completing the challenge. */ @@ -52,7 +49,7 @@ const ReferralSpecifier = z.object({ const TrackSellerSpecifier = z.object({ /** The challenge identifier. As in, the challenge "name." */ - challengeId: z.enum([ChallengeRewardID.AUDIO_MATCHING_BUYER]), + challengeId: z.enum([ChallengeId.AUDIO_MATCHING_BUYER]), /** The user ID of the owner of the track purchased. */ sellerUserId: HashId, /** The track ID that was purchased. */ @@ -61,7 +58,7 @@ const TrackSellerSpecifier = z.object({ const TrackBuyerSpecifier = z.object({ /** The challenge identifier. As in, the challenge "name." */ - challengeId: z.enum([ChallengeRewardID.AUDIO_MATCHING_SELLER]), + challengeId: z.enum([ChallengeId.AUDIO_MATCHING_SELLER]), /** The user ID of the user that bought the track. */ buyerUserId: HashId, /** The track ID that was purchased. */ @@ -80,7 +77,7 @@ export type GenerateSpecifierRequest = z.input export const ClaimRewardsSchema = z .object({ /** The challenge identifier. As in, the challenge "name." */ - challengeId: z.nativeEnum(ChallengeRewardID), + challengeId: z.nativeEnum(ChallengeId), /** * Identifier for the completed challenge instance. * diff --git a/packages/libs/src/sdk/index.ts b/packages/libs/src/sdk/index.ts index a17859131ed..f17b55f65b1 100644 --- a/packages/libs/src/sdk/index.ts +++ b/packages/libs/src/sdk/index.ts @@ -11,10 +11,12 @@ export { DeveloperAppsApi } from './api/developer-apps/DeveloperAppsApi' export { DashboardWalletUsersApi } from './api/dashboard-wallet-users/DashboardWalletUsersApi' export { UsersApi } from './api/users/UsersApi' export { ResolveApi } from './api/ResolveApi' +export { ChallengesApi } from './api/challenges/ChallengesApi' export { GetAudioTransactionHistorySortMethodEnum, GetAudioTransactionHistorySortDirectionEnum } from './api/generated/full' +export * from './api/challenges/types' export * from './api/chats/clientTypes' export * from './api/chats/serverTypes' export * from './api/albums/types' diff --git a/packages/libs/src/sdk/sdkBrowserDist.ts b/packages/libs/src/sdk/sdkBrowserDist.ts index 74f820e4cc7..639b0b19649 100644 --- a/packages/libs/src/sdk/sdkBrowserDist.ts +++ b/packages/libs/src/sdk/sdkBrowserDist.ts @@ -1,3 +1,4 @@ +import { ChallengeId } from './api/challenges/types' import { developmentConfig, productionConfig, stagingConfig } from './config' import { sdk } from './sdk' import { DiscoveryNodeSelector } from './services' @@ -14,3 +15,4 @@ import { ParseRequestError } from './utils/parseParams' ;(window as any).audiusSdk.ParseRequestError = ParseRequestError ;(window as any).audiusSdk.Genre = Genre ;(window as any).audiusSdk.Mood = Mood +;(window as any).audiusSdk.ChallengeId = ChallengeId From ccd75c026d8d7e2e63244ddf7a74bfb27506416d Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 3 Jan 2024 00:30:18 -0600 Subject: [PATCH 49/66] hide new stuff --- packages/libs/src/sdk/api/challenges/ChallengesApi.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index ed45df757ef..fc8618acfee 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -50,6 +50,8 @@ export class ChallengesApi extends BaseAPI { * this includes more data like the referred user ID or purchased track ID * to allow the challenge to be claimed multiple times, but only once per * completed instance. + * + * @hidden */ public async generateSpecifier(request: GenerateSpecifierRequest) { const args = await parseParams( @@ -79,7 +81,7 @@ export class ChallengesApi extends BaseAPI { /** * Claims a reward on behalf of a user. - * + * @hidden * @see {@link generateSpecifier} to create the specifier argument. */ public async claimReward(request: ClaimRewardsRequest) { From 5241a7f33d039fb4f09980d75ca7a01e43b4ecc0 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 3 Jan 2024 22:02:26 -0600 Subject: [PATCH 50/66] fix bug --- packages/libs/src/sdk/api/challenges/ChallengesApi.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index fc8618acfee..46369de933f 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -51,7 +51,7 @@ export class ChallengesApi extends BaseAPI { * to allow the challenge to be claimed multiple times, but only once per * completed instance. * - * @hidden + * @hidden subject to change */ public async generateSpecifier(request: GenerateSpecifierRequest) { const args = await parseParams( @@ -73,7 +73,7 @@ export class ChallengesApi extends BaseAPI { return `${args.buyerUserId}=>${args.trackId}` case ChallengeId.REFERRALS: case ChallengeId.VERIFIED_REFERRALS: - return `${args.userId}=>${args.userId}` + return `${args.userId}=>${args.referredUserId}` default: throw new Error(`Unknown challenge ID: ${args.challengeId}`) } @@ -81,7 +81,9 @@ export class ChallengesApi extends BaseAPI { /** * Claims a reward on behalf of a user. - * @hidden + * + * @hidden subject to change + * * @see {@link generateSpecifier} to create the specifier argument. */ public async claimReward(request: ClaimRewardsRequest) { From 22bfd38342962ef282f516948f450f8b933afbdc Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 9 Jan 2024 22:53:48 -0800 Subject: [PATCH 51/66] Add tests for getUniquelyOwnedEndpoints, add more coverage, fix bug --- .../DiscoveryNodeSelector.test.ts | 317 +++++++++++++++++- .../DiscoveryNodeSelector.ts | 5 +- 2 files changed, 304 insertions(+), 18 deletions(-) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts index d5ea8be5bc8..1e4ab6fbc5c 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts @@ -10,6 +10,7 @@ import type { HealthCheckResponseData } from './healthCheckTypes' import type { DiscoveryNode } from './types' +import shuffle from 'lodash/shuffle' // jest.mock('./healthChecks', () => ({ // getHealthCheck: jest.fn(() => ({})) @@ -27,6 +28,14 @@ const UNHEALTHY_DATA_NODE = 'https://unhealthy-data.audius.co' const UNRESPONSIVE_NODE = 'https://unresponsive.audius.co' const CONTENT_NODE = 'https://contentnode.audius.co' +const generateHealthyNodes = (count: number) => { + const nodes = [] + for (let id = 0; id < count; id++) { + nodes.push(`https://healthy${id}.audius.co`) + } + return nodes +} + const generateSlowerHealthyNodes = (count: number) => { const nodes = [] for (let id = 0; id < count; id++) { @@ -51,28 +60,31 @@ const addDelegateOwnerWallets = (endpoint: string): DiscoveryNode => ({ const NETWORK_DISCOVERY_NODES = [ HEALTHY_NODE, ...generateSlowerHealthyNodes(10) -].map(addDelegateOwnerWallets) +] const healthyComms = { healthy: true } const handlers = [ - rest.get(`${HEALTHY_NODE}/health_check`, (_req, res, ctx) => { - const data: HealthCheckResponseData = { - service: 'discovery-node', - version: '1.2.3', - block_difference: 0, - network: { - discovery_nodes: NETWORK_DISCOVERY_NODES + rest.get( + /https:\/\/healthy(.*).audius.co\/health_check/, + (_req, res, ctx) => { + const data: HealthCheckResponseData = { + service: 'discovery-node', + version: '1.2.3', + block_difference: 0, + network: { + discovery_nodes: NETWORK_DISCOVERY_NODES + } } + return res( + ctx.delay(25), + ctx.status(200), + ctx.json({ data, comms: healthyComms }) + ) } - return res( - ctx.delay(25), - ctx.status(200), - ctx.json({ data, comms: healthyComms }) - ) - }), + ), // Slower healthy rest.get( @@ -144,8 +156,8 @@ const handlers = [ // Unhealthy (offline) rest.get( /https:\/\/unhealthy(.*).audius.co\/health_check/, - (_req, res, ctx) => { - return res(ctx.status(502)) + (_req, res, _ctx) => { + return res.networkError('') } ), @@ -351,6 +363,88 @@ describe('discoveryNodeSelector', () => { expect(selected).toBe(null) }) + test('waits for existing selections to finish gracefully', async () => { + const selector = new DiscoveryNodeSelector({ + bootstrapServices: generateHealthyNodes(100).map(addDelegateOwnerWallets), + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + const selected = await Promise.all([ + selector.getSelectedEndpoint(), + selector.getSelectedEndpoint(), + selector.getSelectedEndpoint(), + selector.getSelectedEndpoint(), + selector.getSelectedEndpoint(), + selector.getSelectedEndpoint() + ]) + expect(selected.every((s) => s === selected[0])).toBe(true) + }) + + test('removes backups when TTL is complete', async () => { + const TEMP_BEHIND_BLOCKDIFF_NODE = 'https://temp-behind.audius.co' + const selector = new DiscoveryNodeSelector({ + backupsTTL: 0, + unhealthyTTL: 0, + bootstrapServices: [TEMP_BEHIND_BLOCKDIFF_NODE, HEALTHY_NODE].map( + addDelegateOwnerWallets + ), + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + let requestCount = 0 + server.use( + rest.get( + `${TEMP_BEHIND_BLOCKDIFF_NODE}/health_check`, + (_req, res, ctx) => { + const data: HealthCheckResponseData = { + service: 'discovery-node', + version: '1.2.3', + block_difference: requestCount++ < 1 ? 50 : 0 // switch to healthy + } + return res(ctx.status(200), ctx.json({ data, comms: healthyComms })) + } + ) + ) + + // First select healthy node + const first = await selector.getSelectedEndpoint() + expect(first).toBe(HEALTHY_NODE) + + const data: ApiHealthResponseData = { + latest_chain_block: 100, + latest_indexed_block: 2, + latest_chain_slot_plays: 100, + latest_indexed_slot_plays: 100, + version: { + service: 'discovery-node', + version: '1.2.3' + } + } + const middleware = selector.createMiddleware() + + // Trigger cleanup by retriggering selection + await middleware.post!({ + fetch, + url: `${HEALTHY_NODE}/v1/full/tracks`, + init: {}, + response: new Response(JSON.stringify(data)) + }) + + // Force reselection + await middleware.post!({ + fetch, + url: `${HEALTHY_NODE}/v1/full/tracks`, + init: {}, + response: new Response(JSON.stringify(data)) + }) + + // Is up to date now + const second = await selector.getSelectedEndpoint() + expect(second).toBe(TEMP_BEHIND_BLOCKDIFF_NODE) + }) + describe('middleware', () => { test('prepends URL to requests', async () => { const selector = new DiscoveryNodeSelector({ @@ -593,5 +687,196 @@ describe('discoveryNodeSelector', () => { }) expect(selector.isBehind).toBe(false) }) + + test('reselects when encountering network error', async () => { + const selector = new DiscoveryNodeSelector({ + bootstrapServices: [BEHIND_BLOCKDIFF_NODE].map(addDelegateOwnerWallets), + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + + const changeHandler = jest.fn(() => {}) + selector.addEventListener('change', changeHandler) + + let requestCount = 0 + server.use( + rest.get(`${BEHIND_BLOCKDIFF_NODE}/health_check`, (_req, res, ctx) => { + const data: HealthCheckResponseData = { + service: 'discovery-node', + version: '1.2.3', + block_difference: 0, + network: { + discovery_nodes: [HEALTHY_NODE, BEHIND_BLOCKDIFF_NODE], + discovery_nodes_with_owner: [ + HEALTHY_NODE, + BEHIND_BLOCKDIFF_NODE + ].map(addDelegateOwnerWallets) + } + } + return requestCount++ > 0 + ? res.networkError('') + : res( + ctx.delay(25), + ctx.status(200), + ctx.json({ data, comms: healthyComms }) + ) + }) + ) + + const middleware = selector.createMiddleware() + await middleware.onError!({ + fetch, + url: `${BEHIND_BLOCKDIFF_NODE}/v1/full/tracks`, + init: {}, + error: '' + }) + expect(changeHandler).toHaveBeenCalled() + const selected = await selector.getSelectedEndpoint() + expect(selected).not.toBe(BEHIND_BLOCKDIFF_NODE) + }) + + test('does not reselect for client error status', async () => { + const selector = new DiscoveryNodeSelector({ + bootstrapServices: NETWORK_DISCOVERY_NODES.map(addDelegateOwnerWallets), + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + + const before = await selector.getSelectedEndpoint() + + const changeHandler = jest.fn(() => {}) + selector.addEventListener('change', changeHandler) + + const middleware = selector.createMiddleware() + await middleware.post!({ + fetch, + url: `${HEALTHY_NODE}/v1/full/tracks`, + init: {}, + response: new Response(null, { status: 404 }) + }) + expect(changeHandler).not.toHaveBeenCalled() + const selected = await selector.getSelectedEndpoint() + expect(selected).toBe(before) + }) + }) + + describe('getUniquelyOwnedEndpoints', () => { + test('gets three uniquely owned discovery nodes', async () => { + const operators = ['0x1', '0x2', '0x3', '0x4'] as const + const healthyNodes = generateHealthyNodes(3) + const unhealthyNodes = generateUnhealthyNodes(50) + const bootstrapServices = shuffle([ + ...unhealthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })), + ...healthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })) + ]) + + const selector = new DiscoveryNodeSelector({ + bootstrapServices, + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + const nodes = await selector.getUniquelyOwnedEndpoints(3) + expect(nodes.length).toBe(3) + expect(nodes.every((n) => n === HEALTHY_NODE)) + }) + + test('filters to allowlist', async () => { + const operators = ['0x1', '0x2', '0x3', '0x4'] as const + const healthyNodes = generateHealthyNodes(3) + const unhealthyNodes = generateUnhealthyNodes(10) + const bootstrapServices = shuffle([ + ...unhealthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })), + ...healthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })) + ]) + + const selector = new DiscoveryNodeSelector({ + bootstrapServices, + unhealthyTTL: 0, + allowlist: new Set(unhealthyNodes), + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + + await expect(async () => { + await selector.getUniquelyOwnedEndpoints(3) + }).rejects.toThrow( + new Error('Not enough healthy services to choose from') + ) + }) + + test('filters to blocklist', async () => { + const operators = ['0x1', '0x2', '0x3', '0x4'] as const + const healthyNodes = generateHealthyNodes(3) + const unhealthyNodes = generateUnhealthyNodes(10) + const bootstrapServices = shuffle([ + ...unhealthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })), + ...healthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })) + ]) + + const selector = new DiscoveryNodeSelector({ + bootstrapServices, + unhealthyTTL: 0, + blocklist: new Set(healthyNodes.slice(0, 1)), + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + + await expect(async () => { + const res = await selector.getUniquelyOwnedEndpoints(3) + console.log(res) + }).rejects.toThrow( + new Error('Not enough healthy services to choose from') + ) + }) + + test('fails when not enough owners', async () => { + const operators = ['0x1', '0x2'] as const + const healthyNodes = generateHealthyNodes(3) + const unhealthyNodes = generateUnhealthyNodes(10) + const bootstrapServices = shuffle([ + ...unhealthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })), + ...healthyNodes.map((endpoint, i) => ({ + endpoint, + delegateOwnerWallet: operators[i % operators.length]! + })) + ]) + + const selector = new DiscoveryNodeSelector({ + bootstrapServices, + healthCheckThresholds: { + minVersion: '1.2.3' + } + }) + + await expect(async () => { + await selector.getUniquelyOwnedEndpoints(3) + }).rejects.toThrow(new Error('Not enough unique owners to choose from')) + }) }) }) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index d909bc61967..4aa5abecc75 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -33,6 +33,7 @@ import { ServiceSelectionEvents, DiscoveryNode } from './types' +import sample from 'lodash/sample' const getPathFromUrl = (url: string) => { const pathRegex = /^([a-z]+:\/\/)?(?:www\.)?([^/]+)?(.*)$/ @@ -339,8 +340,8 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { const selectedNodes: string[] = [] while (ownersToTry.size > 0 && selectedNodes.length < nodeCount) { // Get an unattempted owner - const [sampledOwner] = sampleSize(Object.keys(endpointsByOwner), 1) - ownersToTry.delete(sampledOwner!) // always defined + const sampledOwner = sample([...ownersToTry]) + ownersToTry.delete(sampledOwner!) // Get the owner's services const servicesToTry = new Set(endpointsByOwner[sampledOwner!]) From ae1e01a72a74b99bf8ad729492388773f23951f0 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 9 Jan 2024 23:42:47 -0800 Subject: [PATCH 52/66] Improve AAO selector --- packages/libs/src/sdk/config/development.ts | 2 +- packages/libs/src/sdk/config/production.ts | 6 +- packages/libs/src/sdk/config/staging.ts | 2 +- packages/libs/src/sdk/config/types.ts | 2 +- .../src/sdk/scripts/generateServicesConfig.ts | 4 +- .../AntiAbuseOracleSelector.test.ts | 89 +++++++++++++++++++ .../AntiAbuseOracleSelector.ts | 70 +++++++++------ .../services/AntiAbuseOracleSelector/types.ts | 21 ++++- 8 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts diff --git a/packages/libs/src/sdk/config/development.ts b/packages/libs/src/sdk/config/development.ts index 1d570cc06d3..2157bea4b39 100644 --- a/packages/libs/src/sdk/config/development.ts +++ b/packages/libs/src/sdk/config/development.ts @@ -25,7 +25,7 @@ export const servicesConfig: ServicesConfig = { "endpoints": [ "http://audius-protocol-anti-abuse-oracle-1:8000" ], - "addresses": [ + "registeredAddresses": [ "0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3" ] } diff --git a/packages/libs/src/sdk/config/production.ts b/packages/libs/src/sdk/config/production.ts index a8d8591c5aa..d009b225982 100644 --- a/packages/libs/src/sdk/config/production.ts +++ b/packages/libs/src/sdk/config/production.ts @@ -246,6 +246,10 @@ export const servicesConfig: ServicesConfig = { { "endpoint": "https://audius.w3coins.io", "delegateOwnerWallet": "0x1cBC80a8A4A6fd5793e535B758A71f332DEc1551" + }, + { + "endpoint": "https://audius-nodes.com", + "delegateOwnerWallet": "0xE83699015c8eb793A0678eA7dC398ac58f7928c4" } ], "storageNodes": [ @@ -532,7 +536,7 @@ export const servicesConfig: ServicesConfig = { "https://audius-oracle.creatorseed.com", "https://oracle.audius.endl.net" ], - "addresses": [ + "registeredAddresses": [ "0x9811BA3eAB1F2Cd9A2dFeDB19e8c2a69729DC8b6", "0xe60d50356cd891f56B744165fcc1D8B352201A76", "0x7A03cFAE79266683D9706731D6E187868E939c9C" diff --git a/packages/libs/src/sdk/config/staging.ts b/packages/libs/src/sdk/config/staging.ts index 81aab511a3b..7a662c263fc 100644 --- a/packages/libs/src/sdk/config/staging.ts +++ b/packages/libs/src/sdk/config/staging.ts @@ -62,7 +62,7 @@ export const servicesConfig: ServicesConfig = { "endpoints": [ "https://antiabuseoracle.staging.audius.co" ], - "addresses": [ + "registeredAddresses": [ "0x00b6462e955dA5841b6D9e1E2529B830F00f31Bf", "0x57B57efFA54ba37DBF8A06B9c42E7611e84BDe6F", "0xF617bbc0913bAE0a13f6D4A19eCDE5Aa07B0fF0A" diff --git a/packages/libs/src/sdk/config/types.ts b/packages/libs/src/sdk/config/types.ts index 1ce6a8beaa0..2f947426f00 100644 --- a/packages/libs/src/sdk/config/types.ts +++ b/packages/libs/src/sdk/config/types.ts @@ -6,7 +6,7 @@ export type ServicesConfig = { storageNodes: StorageNode[] antiAbuseOracleNodes: { endpoints: string[] - addresses: string[] + registeredAddresses: string[] } entityManagerContractAddress: string web3ProviderUrl: string diff --git a/packages/libs/src/sdk/scripts/generateServicesConfig.ts b/packages/libs/src/sdk/scripts/generateServicesConfig.ts index 79cf5651563..d3dbf2d3a5c 100644 --- a/packages/libs/src/sdk/scripts/generateServicesConfig.ts +++ b/packages/libs/src/sdk/scripts/generateServicesConfig.ts @@ -76,7 +76,7 @@ const devConfig: ServicesConfig = { identityServiceUrl: 'http://audius-protocol-identity-service-1', antiAbuseOracleNodes: { endpoints: ['http://audius-protocol-anti-abuse-oracle-1:8000'], - addresses: ['0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3'] + registeredAddresses: ['0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3'] } } @@ -128,7 +128,7 @@ const generateServicesConfig = async ( } const antiAbuseOracleNodes = { endpoints: config.AAO_ENDPOINTS, - addresses: antiAbuseAddresses + registeredAddresses: antiAbuseAddresses } const minVersion = await contracts.getCurrentVersion('discovery-node') diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts new file mode 100644 index 00000000000..717c4aa6b60 --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts @@ -0,0 +1,89 @@ +import { rest } from 'msw' +import { AntiAbuseOracleSelector } from './AntiAbuseOracleSelector' +import { setupServer } from 'msw/node' + +const HEALTHY_NODE = 'https://healthy-aao.audius.co' +const OFFLINE_NODE = 'https://offline-aao.audius.co' +const UNHEALTHY_NODE = 'https://unhealthy-aao.audius.co' +const UNREGISTERED_NODE = 'https://unregistered-aao.audius.co' +const registeredAddresses = ['0x11111111'] + +const handlers = [ + rest.get(`${HEALTHY_NODE}/health_check`, (_req, res, ctx) => { + return res( + ctx.json({ + walletPubkey: registeredAddresses[0] + }) + ) + }), + rest.get(`${OFFLINE_NODE}/health_check`, (_req, res, _ctx) => { + return res.networkError('') + }), + rest.get(`${UNHEALTHY_NODE}/health_check`, (_req, res, ctx) => { + return res(ctx.status(500)) + }), + rest.get(`${UNREGISTERED_NODE}/health_check`, (_req, res, ctx) => { + return res( + ctx.json({ + walletPubkey: '0x12345' + }) + ) + }) +] +const server = setupServer(...handlers) + +describe('AntiAbuseOracleSelector', () => { + beforeAll(() => { + server.listen() + }) + + afterEach(() => server.resetHandlers()) + + afterAll(() => server.close()) + + it('does not choose unhealthy nodes', async () => { + const selector = new AntiAbuseOracleSelector({ + endpoints: [UNHEALTHY_NODE, HEALTHY_NODE], + registeredAddresses + }) + const selected = await selector.getSelectedService() + expect(selected.endpoint).not.toBe(UNHEALTHY_NODE) + }) + + it('does not choose offline nodes', async () => { + const selector = new AntiAbuseOracleSelector({ + endpoints: [OFFLINE_NODE, HEALTHY_NODE], + registeredAddresses + }) + const selected = await selector.getSelectedService() + expect(selected.endpoint).not.toBe(UNHEALTHY_NODE) + }) + + it('does not choose unregistered nodes', async () => { + const selector = new AntiAbuseOracleSelector({ + endpoints: [UNHEALTHY_NODE, HEALTHY_NODE], + registeredAddresses + }) + const selected = await selector.getSelectedService() + expect(selected.endpoint).not.toBe(UNHEALTHY_NODE) + }) + + it('chooses a healthy node', async () => { + const selector = new AntiAbuseOracleSelector({ + endpoints: [UNHEALTHY_NODE, UNREGISTERED_NODE, HEALTHY_NODE], + registeredAddresses + }) + const selected = await selector.getSelectedService() + expect(selected.endpoint).toBe(HEALTHY_NODE) + }) + + it('throws if none available', async () => { + const selector = new AntiAbuseOracleSelector({ + endpoints: [UNHEALTHY_NODE, UNREGISTERED_NODE, OFFLINE_NODE], + registeredAddresses + }) + await expect(async () => { + await selector.getSelectedService() + }).rejects.toThrow() + }) +}) diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts index f21f87b198c..3e28c0e678d 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts @@ -1,4 +1,4 @@ -import { sampleSize } from 'lodash' +import sample from 'lodash/sample' import { mergeConfigWithDefaults } from '../../utils/mergeConfigs' import type { LoggerService } from '../Logger' import { defaultAntiAbuseOracleSelectorConfig } from './constants' @@ -9,49 +9,65 @@ import type { } from './types' export class AntiAbuseOracleSelector implements AntiAbuseOracleSelectorService { - private services: AntiAbuseOracle[] | null = null - private readonly logger: LoggerService private readonly endpoints: string[] - private readonly addresses: string[] + private readonly registeredAddresses: string[] + private readonly logger: LoggerService + private selectedNode: AntiAbuseOracle | null = null + private services: AntiAbuseOracle[] | null = null + constructor(config?: AntiAbuseOracleSelectorConfig) { const configWithDefaults = mergeConfigWithDefaults( config, defaultAntiAbuseOracleSelectorConfig ) this.endpoints = configWithDefaults.endpoints - this.addresses = configWithDefaults.addresses + this.registeredAddresses = configWithDefaults.registeredAddresses this.logger = configWithDefaults.logger } + /** + * Finds a healthy, registered Anti Abuse Oracle. + */ public async getSelectedService() { - const services = await this.getHealthyRegisteredServices() - return sampleSize(services, 1)[0] ?? null + if (!this.selectedNode) { + const services = await this.getServices() + const service = sample(services) + if (!service) { + throw new Error('All Anti Abuse Oracles are unhealthy') + } + this.selectedNode = service + } + return this.selectedNode } - private async getHealthyRegisteredServices() { + private async getServices() { if (this.services === null) { this.services = [] - for (const endpoint of this.endpoints) { - try { - const response = await fetch(`${endpoint}/health_check`) - if (response.ok) { - const json = await response.json() - const wallet = json.walletPubkey - if (!this.addresses.includes(wallet)) { - throw new Error(`Not registered: ${wallet}`) - } - this.services.push({ - endpoint, - wallet - }) - } else { - throw new Error(`Response failed with status ${response.status}`) + await Promise.all( + this.endpoints.map(async (endpoint) => { + try { + const service = await this.checkHealth(endpoint) + this.services?.push(service) + } catch (e) { + this.logger.warn(`Anti Abuse Oracle ${endpoint} is unhealthy: ${e}`) } - } catch (e) { - this.logger.warn(`Anti Abuse Oracle ${endpoint} is unhealthy: ${e}`) - } - } + }) + ) } return this.services } + + private async checkHealth(endpoint: string) { + const response = await fetch(`${endpoint}/health_check`) + if (response.ok) { + const json = await response.json() + const wallet = json.walletPubkey + if (!this.registeredAddresses.includes(wallet)) { + throw new Error(`Not registered: ${wallet}`) + } + return { wallet, endpoint } + } else { + throw new Error(`Response failed with status ${response.status}`) + } + } } diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts index 90eac61095d..d9b87fef339 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts @@ -1,17 +1,34 @@ import type { LoggerService } from '../Logger' export type AntiAbuseOracle = { + /** The wallet address of the Anti Abuse Oracle. */ wallet: string + /** The URL of the Anti Abuse Oracle. */ endpoint: string } +/** + * Service used to choose an Anti Abuse Oracle service. + */ export type AntiAbuseOracleSelectorService = { - getSelectedService(): Promise + /** + * Gets a registered, healthy Anti Abuse Oracle endpoint and wallet. + */ + getSelectedService(): Promise } +/** + * Note that for Anti Abuse Oracle, the wallet is the only information stored + * on-chain. The URL endpoint is off-chain information. Thus, this configuration + * must be complete as opposed to the "bootstrap" nodes of Discovery or Storage, + * which are only used in initial selection. + */ export type AntiAbuseOracleSelectorConfigInternal = { + /** All possible endpoints for Anti Abuse Oracles. */ endpoints: string[] - addresses: string[] + /** All registered wallet addresses of Anti Abuse Oracles. */ + registeredAddresses: string[] + /** The logging service. */ logger: LoggerService } From 4198b081cbc8e1212cca37cc6eb38c5a36e61d4c Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 10 Jan 2024 00:21:45 -0800 Subject: [PATCH 53/66] Rename AntiAbuseOracleApi, move to service --- .../api/antiAbuseOracle/AntiAbuseOracleApi.ts | 35 ------------------ .../src/sdk/api/challenges/ChallengesApi.ts | 8 ++-- .../AntiAbuseOracle/AntiAbuseOracle.ts | 33 +++++++++++++++++ .../src/sdk/services/AntiAbuseOracle/types.ts | 37 +++++++++++++++++++ .../AntiAbuseOracleSelector.ts | 6 +-- .../services/AntiAbuseOracleSelector/types.ts | 4 +- 6 files changed, 79 insertions(+), 44 deletions(-) delete mode 100644 packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts create mode 100644 packages/libs/src/sdk/services/AntiAbuseOracle/types.ts diff --git a/packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts b/packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts deleted file mode 100644 index 9ca32b8ac68..00000000000 --- a/packages/libs/src/sdk/api/antiAbuseOracle/AntiAbuseOracleApi.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BaseAPI, JSONApiResponse, exists } from '../generated/default' - -export class AntiAbuseOracleApi extends BaseAPI { - public async getChallengeAttestation({ - handle, - challengeId, - specifier, - amount - }: { - handle: string - challengeId: string - specifier: string - amount: number - }) { - const response = await this.request({ - path: `/attestation/${handle}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: { - challengeId, - challengeSpecifier: specifier, - amount - } - }) - return await new JSONApiResponse<{ - signature: string | false - errorCode?: number - }>(response, (json) => ({ - signature: exists(json, 'result') ? json.result : false, - errorCode: exists(json, 'errorCode') ? json.errorCode : undefined - })).value() - } -} diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 46369de933f..2da5134c7b4 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -5,7 +5,7 @@ import type { LoggerService } from '../../services' import type { - AntiAbuseOracle, + AntiAbuseOracleNode, AntiAbuseOracleSelectorService } from '../../services/AntiAbuseOracleSelector/types' import type { RewardManagerClient } from '../../services/Solana/programs/RewardManagerClient/RewardManagerClient' @@ -24,7 +24,7 @@ import { GenerateSpecifierSchema } from './types' import type { PublicKey } from '@solana/web3.js' -import { AntiAbuseOracleApi } from '../antiAbuseOracle/AntiAbuseOracleApi' +import { AntiAbuseOracle } from '../../services/AntiAbuseOracle/AntiAbuseOracle' import { toChecksumAddress } from 'ethereumjs-util' export class ChallengesApi extends BaseAPI { @@ -189,14 +189,14 @@ export class ChallengesApi extends BaseAPI { recipientEthAddress, handle }: { - antiAbuseOracle: AntiAbuseOracle + antiAbuseOracle: AntiAbuseOracleNode challengeId: ChallengeId specifier: string amount: bigint recipientEthAddress: string handle: string }) { - const antiAbuseOracleAttestation = await new AntiAbuseOracleApi( + const antiAbuseOracleAttestation = await new AntiAbuseOracle( new Configuration({ basePath: antiAbuseOracle.endpoint }) ).getChallengeAttestation({ handle, diff --git a/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts b/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts new file mode 100644 index 00000000000..2e422b19a73 --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts @@ -0,0 +1,33 @@ +import { BaseAPI, JSONApiResponse, exists } from '../../api/generated/default' +import { parseParams } from '../../utils/parseParams' +import { + AntiAbuseOracleService, + AttestationRequest, + AttestationResponse, + GetAttestationSchema +} from './types' + +export class AntiAbuseOracle extends BaseAPI implements AntiAbuseOracleService { + public async getChallengeAttestation(args: AttestationRequest) { + const { handle, challengeId, specifier, amount } = await parseParams( + 'getChallengeAttestation', + GetAttestationSchema + )(args) + const response = await this.request({ + path: `/attestation/${handle}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: { + challengeId, + challengeSpecifier: specifier, + amount + } + }) + return await new JSONApiResponse(response, (json) => ({ + signature: exists(json, 'result') ? json.result : false, + errorCode: exists(json, 'errorCode') ? json.errorCode : undefined + })).value() + } +} diff --git a/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts new file mode 100644 index 00000000000..6210c4cd506 --- /dev/null +++ b/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' +import { ChallengeId } from '../../api/challenges/types' +// @ts-ignore:next-line ignore the unused import, used in jsdoc +import type { ChallengesApi } from '../../api/challenges/ChallengesApi' + +export const GetAttestationSchema = z.object({ + /** The user's handle. */ + handle: z.string(), + /** The challenge identifier. As in, the challenge "name." */ + challengeId: z.nativeEnum(ChallengeId), + /** + * Identifier for the completed challenge instance. + * + * @see {@link ChallengesApi.generateSpecifier} + */ + specifier: z.string(), + /** The amount being claimed, in decimal wAUDIO. */ + amount: z.number() +}) + +export type AttestationRequest = z.input + +export type AttestationResponse = { + /** The signature attesting to the challenge redemption, or false. */ + signature: string | false + /** The error code of the failed attestation, if applicable. */ + errorCode?: number +} + +/** + * API Client for the Anti Abuse Oracle Service. + */ +export type AntiAbuseOracleService = { + getChallengeAttestation: ( + args: AttestationRequest + ) => Promise +} diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts index 3e28c0e678d..e081d636d93 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts @@ -4,7 +4,7 @@ import type { LoggerService } from '../Logger' import { defaultAntiAbuseOracleSelectorConfig } from './constants' import type { AntiAbuseOracleSelectorService, - AntiAbuseOracle, + AntiAbuseOracleNode, AntiAbuseOracleSelectorConfig } from './types' @@ -12,8 +12,8 @@ export class AntiAbuseOracleSelector implements AntiAbuseOracleSelectorService { private readonly endpoints: string[] private readonly registeredAddresses: string[] private readonly logger: LoggerService - private selectedNode: AntiAbuseOracle | null = null - private services: AntiAbuseOracle[] | null = null + private selectedNode: AntiAbuseOracleNode | null = null + private services: AntiAbuseOracleNode[] | null = null constructor(config?: AntiAbuseOracleSelectorConfig) { const configWithDefaults = mergeConfigWithDefaults( diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts index d9b87fef339..d4fcd429543 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts @@ -1,6 +1,6 @@ import type { LoggerService } from '../Logger' -export type AntiAbuseOracle = { +export type AntiAbuseOracleNode = { /** The wallet address of the Anti Abuse Oracle. */ wallet: string /** The URL of the Anti Abuse Oracle. */ @@ -14,7 +14,7 @@ export type AntiAbuseOracleSelectorService = { /** * Gets a registered, healthy Anti Abuse Oracle endpoint and wallet. */ - getSelectedService(): Promise + getSelectedService(): Promise } /** From cab52b5750c684b39db4e6f6268060619beea2f8 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 10 Jan 2024 16:52:04 -0800 Subject: [PATCH 54/66] Refactor AntiAbuseOracle --- .../src/sdk/api/challenges/ChallengesApi.ts | 56 +++++++-------- .../libs/src/sdk/api/users/UsersApi.test.ts | 2 +- packages/libs/src/sdk/sdk.ts | 31 ++++---- .../AntiAbuseOracle/AntiAbuseOracle.ts | 70 ++++++++++++++----- .../src/sdk/services/AntiAbuseOracle/types.ts | 35 +++++++++- .../AntiAbuseOracleSelector.ts | 68 +++++++++++++----- .../services/AntiAbuseOracleSelector/types.ts | 6 ++ .../DiscoveryNodeSelector.ts | 14 +--- .../src/sdk/services/Solana/SolanaRelay.ts | 5 +- .../libs/src/sdk/services/Solana/types.ts | 14 ++-- .../StorageNodeSelector.test.ts | 4 +- packages/libs/src/sdk/types.ts | 6 ++ packages/libs/src/sdk/utils/getPathFromUrl.ts | 15 ++++ 13 files changed, 220 insertions(+), 106 deletions(-) create mode 100644 packages/libs/src/sdk/utils/getPathFromUrl.ts diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 2da5134c7b4..4fb11fdc99a 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -4,10 +4,6 @@ import type { DiscoveryNodeSelectorService, LoggerService } from '../../services' -import type { - AntiAbuseOracleNode, - AntiAbuseOracleSelectorService -} from '../../services/AntiAbuseOracleSelector/types' import type { RewardManagerClient } from '../../services/Solana/programs/RewardManagerClient/RewardManagerClient' import { parseParams } from '../../utils/parseParams' import { BaseAPI, Configuration } from '../generated/default' @@ -24,7 +20,7 @@ import { GenerateSpecifierSchema } from './types' import type { PublicKey } from '@solana/web3.js' -import { AntiAbuseOracle } from '../../services/AntiAbuseOracle/AntiAbuseOracle' +import { AntiAbuseOracleService } from '../../services/AntiAbuseOracle/types' import { toChecksumAddress } from 'ethereumjs-util' export class ChallengesApi extends BaseAPI { @@ -35,7 +31,7 @@ export class ChallengesApi extends BaseAPI { private readonly discoveryNodeSelector: DiscoveryNodeSelectorService, private readonly rewardManager: RewardManagerClient, private readonly claimableTokens: ClaimableTokensClient, - private readonly antiAbuseOracleSelector: AntiAbuseOracleSelectorService, + private readonly antiAbuseOracle: AntiAbuseOracleService, logger: LoggerService ) { super(config) @@ -118,24 +114,16 @@ export class ChallengesApi extends BaseAPI { (m) => m.attestation.antiAbuseOracleEthAddress === null )?.senderEthAddress if (!antiAbuseOracleEthAddress) { - this.logger.debug('Selecting anti abuse oracle for attestation...') - const antiAbuseOracle = - await this.antiAbuseOracleSelector.getSelectedService() - if (!antiAbuseOracle) { - throw new Error('Could not find a healthy anti abuse oracle.') - } - antiAbuseOracleEthAddress = antiAbuseOracle.wallet - this.logger.debug('Submitting anti abuse oracle attestation...') - const signature = await this.submitAntiAbuseOracleAttestation({ - antiAbuseOracle, + const response = await this.submitAntiAbuseOracleAttestation({ challengeId, specifier, amount, recipientEthAddress, handle }) - attestationTransactionSignatures.push(signature) + antiAbuseOracleEthAddress = response.antiAbuseOracleEthAddress + attestationTransactionSignatures.push(response.transactionSignature) } else { // Need to convert to checksum address as the attestation is lowercased antiAbuseOracleEthAddress = toChecksumAddress(antiAbuseOracleEthAddress) @@ -182,29 +170,28 @@ export class ChallengesApi extends BaseAPI { } private async submitAntiAbuseOracleAttestation({ - antiAbuseOracle, challengeId, specifier, amount, recipientEthAddress, handle }: { - antiAbuseOracle: AntiAbuseOracleNode challengeId: ChallengeId specifier: string amount: bigint recipientEthAddress: string handle: string }) { - const antiAbuseOracleAttestation = await new AntiAbuseOracle( - new Configuration({ basePath: antiAbuseOracle.endpoint }) - ).getChallengeAttestation({ - handle, - challengeId, - specifier, - amount: Number(wAUDIO(amount).toString()) - }) - if (!antiAbuseOracleAttestation.signature) { + const antiAbuseOracleAttestation = + await this.antiAbuseOracle.getChallengeAttestation({ + handle, + challengeId, + specifier, + amount: Number(wAUDIO(amount).toString()) + }) + const antiAbuseOracleEthAddress = + await this.antiAbuseOracle.getWalletAddress() + if (!antiAbuseOracleAttestation.result) { throw new Error('Failed to get AAO attestation') } const aaoSubmitSecpInstruction = @@ -213,19 +200,24 @@ export class ChallengesApi extends BaseAPI { specifier, amount, recipientEthAddress, - senderEthAddress: antiAbuseOracle.wallet, - senderSignature: antiAbuseOracleAttestation.signature + senderEthAddress: antiAbuseOracleEthAddress, + senderSignature: antiAbuseOracleAttestation.result }) const aaoSubmitInstruction = await this.rewardManager.createSubmitAttestationInstruction({ challengeId, specifier, - senderEthAddress: antiAbuseOracle.wallet + senderEthAddress: antiAbuseOracleEthAddress }) const submitAAOTransaction = await this.rewardManager.buildTransaction({ instructions: [aaoSubmitSecpInstruction, aaoSubmitInstruction] }) - return await this.rewardManager.sendTransaction(submitAAOTransaction) + return { + transactionSignature: await this.rewardManager.sendTransaction( + submitAAOTransaction + ), + antiAbuseOracleEthAddress + } } private async submitDiscoveryAttestations({ diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index 82a30eeb98a..72f671857e7 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -67,7 +67,7 @@ describe('UsersApi', () => { discoveryNodeSelector, logger }) - const solanaRelay = new SolanaRelay() + const solanaRelay = new SolanaRelay({ discoveryNodeSelector }) const claimableTokens = new ClaimableTokensClient({ solanaWalletAdapter: new SolanaRelayWalletAdapter({ solanaRelay }) }) diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index 4a0bad6a98a..c1478012737 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -42,6 +42,7 @@ import { defaultClaimableTokensConfig } from './services/Solana/programs/Claimab import { defaultRewardManagerClentConfig } from './services/Solana/programs/RewardManagerClient/constants' import { AntiAbuseOracleSelector } from './services/AntiAbuseOracleSelector/AntiAbuseOracleSelector' import { ChallengesApi } from './api/challenges/ChallengesApi' +import { AntiAbuseOracle } from './services/AntiAbuseOracle/AntiAbuseOracle' /** * The Audius SDK @@ -90,32 +91,31 @@ const initializeServices = (config: SdkConfig) => { ? new AppAuth(config.apiKey, config.apiSecret) : new Auth() - const defaultDiscoveryNodeSelector = new DiscoveryNodeSelector({ logger }) + const discoveryNodeSelector = + config.services?.discoveryNodeSelector ?? + new DiscoveryNodeSelector({ logger }) const storageNodeSelector = config.services?.storageNodeSelector ?? new StorageNodeSelector({ auth: config.services?.auth ?? defaultAuthService, - discoveryNodeSelector: - config.services?.discoveryNodeSelector ?? defaultDiscoveryNodeSelector, + discoveryNodeSelector, logger }) const defaultEntityManager = new EntityManager({ ...defaultEntityManagerConfig, - discoveryNodeSelector: - config.services?.discoveryNodeSelector ?? defaultDiscoveryNodeSelector + discoveryNodeSelector }) const defaultStorage = new Storage({ storageNodeSelector, logger }) - const defaultAntiAbuseOracleSelector = new AntiAbuseOracleSelector({ logger }) + const antiAbuseOracleSelector = + config.services?.antiAbuseOracleSelector ?? + new AntiAbuseOracleSelector({ logger }) const defaultSolanaRelay = new SolanaRelay({ - middleware: [ - config.services?.discoveryNodeSelector?.createMiddleware() ?? - defaultDiscoveryNodeSelector.createMiddleware() - ] + discoveryNodeSelector }) const defaultSolanaWalletAdapter = new SolanaRelayWalletAdapter({ @@ -134,9 +134,14 @@ const initializeServices = (config: SdkConfig) => { config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter }) + const defaultAntiAbuseOracle = new AntiAbuseOracle({ + antiAbuseOracleSelector + }) + const defaultServices: ServicesContainer = { storageNodeSelector, - discoveryNodeSelector: defaultDiscoveryNodeSelector, + discoveryNodeSelector, + antiAbuseOracleSelector, entityManager: defaultEntityManager, storage: defaultStorage, auth: defaultAuthService, @@ -144,7 +149,7 @@ const initializeServices = (config: SdkConfig) => { rewardManagerProgram: defaultRewardManagerProgram, solanaWalletAdapter: defaultSolanaWalletAdapter, solanaRelay: defaultSolanaRelay, - antiAbuseOracleSelector: defaultAntiAbuseOracleSelector, + antiAbuseOracle: defaultAntiAbuseOracle, logger } return { ...defaultServices, ...config.services } @@ -233,7 +238,7 @@ const initializeApis = ({ services.discoveryNodeSelector, services.rewardManagerProgram, services.claimableTokensProgram, - services.antiAbuseOracleSelector, + services.antiAbuseOracle, services.logger ) diff --git a/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts b/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts index 2e422b19a73..16e6a575ff1 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts @@ -1,33 +1,71 @@ import { BaseAPI, JSONApiResponse, exists } from '../../api/generated/default' +import * as runtime from '../../api/generated/default/runtime' import { parseParams } from '../../utils/parseParams' +import { AntiAbuseOracleSelectorService } from '../AntiAbuseOracleSelector' import { + AntiAbuseOracleConfig, AntiAbuseOracleService, AttestationRequest, AttestationResponse, GetAttestationSchema } from './types' +import fetch from 'cross-fetch' export class AntiAbuseOracle extends BaseAPI implements AntiAbuseOracleService { - public async getChallengeAttestation(args: AttestationRequest) { + private readonly antiAbuseOracleSelector: AntiAbuseOracleSelectorService + + constructor({ antiAbuseOracleSelector }: AntiAbuseOracleConfig) { + super( + new runtime.Configuration({ + fetchApi: fetch, + middleware: [antiAbuseOracleSelector.createMiddleware()] + }) + ) + this.antiAbuseOracleSelector = antiAbuseOracleSelector + } + + public async getWalletAddress() { + const selected = await this.antiAbuseOracleSelector.getSelectedService() + return selected.wallet + } + + public async getChallengeAttestation( + params: AttestationRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction + ) { + const response = await this.getChallengeAttestationRaw( + params, + initOverrides + ) + return await response.value() + } + + public async getChallengeAttestationRaw( + params: AttestationRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction + ) { const { handle, challengeId, specifier, amount } = await parseParams( 'getChallengeAttestation', GetAttestationSchema - )(args) - const response = await this.request({ - path: `/attestation/${handle}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json' + )(params) + const response = await this.request( + { + path: `/attestation/${handle}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: { + challengeId, + challengeSpecifier: specifier, + amount + } }, - body: { - challengeId, - challengeSpecifier: specifier, - amount - } - }) - return await new JSONApiResponse(response, (json) => ({ - signature: exists(json, 'result') ? json.result : false, + initOverrides + ) + return new JSONApiResponse(response, (json) => ({ + result: exists(json, 'result') ? json.result : false, errorCode: exists(json, 'errorCode') ? json.errorCode : undefined - })).value() + })) } } diff --git a/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts index 6210c4cd506..51d02f8aa63 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts @@ -2,6 +2,7 @@ import { z } from 'zod' import { ChallengeId } from '../../api/challenges/types' // @ts-ignore:next-line ignore the unused import, used in jsdoc import type { ChallengesApi } from '../../api/challenges/ChallengesApi' +import { AntiAbuseOracleSelectorService } from '../AntiAbuseOracleSelector' export const GetAttestationSchema = z.object({ /** The user's handle. */ @@ -21,17 +22,47 @@ export const GetAttestationSchema = z.object({ export type AttestationRequest = z.input export type AttestationResponse = { - /** The signature attesting to the challenge redemption, or false. */ - signature: string | false + /** The signature attesting the challenge disbursement request is allowed by AAO, or false. */ + result: string | false /** The error code of the failed attestation, if applicable. */ errorCode?: number } +export type AntiAbuseOracleHealthCheckResponse = { + /** The version number of AAO deployed. */ + version: number + /** The discovery node endpoint the AAO is using. */ + selectedDiscoveryNode: string + /** A list of other AAO endpoints. (note: Empty at time of writing!) */ + otherAntiAbuseOracleEndpoints: string[] + /** The wallet public key address for this AAO. */ + walletPubkey: string + /** Whether the database container is healthy (UP) or unhealthy (DOWN). */ + db: 'UP' | 'DOWN' + /** The date of the last successful attestation. */ + lastSuccessfulAttestation: string + /** The date of the last failed attestation. */ + lastfailedAttestation: string +} + /** * API Client for the Anti Abuse Oracle Service. */ export type AntiAbuseOracleService = { + /** + * Gets an attestation from Anti Abuse Oracle that the given user is + * not marked as abusive by our anti abuse mechanisms for the purpose of + * claiming a reward for completing a challenge. + */ getChallengeAttestation: ( args: AttestationRequest ) => Promise + /** + * Gets the wallet address of the current selected node. + */ + getWalletAddress: () => Promise +} + +export type AntiAbuseOracleConfig = { + antiAbuseOracleSelector: AntiAbuseOracleSelectorService } diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts index e081d636d93..d2be47e2bc8 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts @@ -7,13 +7,21 @@ import type { AntiAbuseOracleNode, AntiAbuseOracleSelectorConfig } from './types' +import { + ErrorContext, + Middleware, + RequestContext +} from '../../api/generated/default' +import { getPathFromUrl } from '../../utils/getPathFromUrl' +import { AntiAbuseOracleHealthCheckResponse } from '../AntiAbuseOracle/types' +import fetch from 'cross-fetch' +import { promiseAny } from '../../utils/promiseAny' export class AntiAbuseOracleSelector implements AntiAbuseOracleSelectorService { private readonly endpoints: string[] private readonly registeredAddresses: string[] private readonly logger: LoggerService private selectedNode: AntiAbuseOracleNode | null = null - private services: AntiAbuseOracleNode[] | null = null constructor(config?: AntiAbuseOracleSelectorConfig) { const configWithDefaults = mergeConfigWithDefaults( @@ -25,42 +33,68 @@ export class AntiAbuseOracleSelector implements AntiAbuseOracleSelectorService { this.logger = configWithDefaults.logger } + public createMiddleware(): Middleware { + return { + pre: async (context: RequestContext) => { + let url = context.url + if (!url.startsWith('http')) { + const service = await this.getSelectedService() + url = `${service.endpoint}${context.url}` + } + return { url, init: context.init } + }, + onError: async (context: ErrorContext) => { + // Reselect and retry on new healthy AAO + const path = getPathFromUrl(context.url) + this.selectedNode = null + const newService = await this.getSelectedService() + // Use context.fetch to retry just once. + return context.fetch(`${newService.endpoint}${path}`, context.init) + } + } + } + /** - * Finds a healthy, registered Anti Abuse Oracle. + * Gets the currently selected Anti Abuse Oracle. + * @throws if no service is available. */ public async getSelectedService() { if (!this.selectedNode) { - const services = await this.getServices() - const service = sample(services) - if (!service) { - throw new Error('All Anti Abuse Oracles are unhealthy') - } - this.selectedNode = service + this.selectedNode = await this.select() } return this.selectedNode } - private async getServices() { - if (this.services === null) { - this.services = [] - await Promise.all( + /** + * Races the configured endpoints for the fastest healthy registered service. + * @throws if no services available. + */ + private async select() { + try { + return await promiseAny( this.endpoints.map(async (endpoint) => { try { - const service = await this.checkHealth(endpoint) - this.services?.push(service) + return await this.getNode(endpoint) } catch (e) { this.logger.warn(`Anti Abuse Oracle ${endpoint} is unhealthy: ${e}`) + throw e } }) ) + } catch (e) { + throw new Error('All Anti Abuse Oracles are unhealthy') } - return this.services } - private async checkHealth(endpoint: string) { + /** + * Fetches the healthcheck for the given endpoint, and checks that the wallet + * is a registered Anti Abuse Oracle wallet. + * @returns the node wallet and endpoint if healthy + */ + private async getNode(endpoint: string) { const response = await fetch(`${endpoint}/health_check`) if (response.ok) { - const json = await response.json() + const json: AntiAbuseOracleHealthCheckResponse = await response.json() const wallet = json.walletPubkey if (!this.registeredAddresses.includes(wallet)) { throw new Error(`Not registered: ${wallet}`) diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts index d4fcd429543..3e43ab309bd 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/types.ts @@ -1,3 +1,4 @@ +import { Middleware } from '../../api/generated/default' import type { LoggerService } from '../Logger' export type AntiAbuseOracleNode = { @@ -11,6 +12,11 @@ export type AntiAbuseOracleNode = { * Service used to choose an Anti Abuse Oracle service. */ export type AntiAbuseOracleSelectorService = { + /** + * Returns a middleware that prepends the Anti Abuse Oracle endpoint to each + * request and can reselect on certain responses or errors. + */ + createMiddleware: () => Middleware /** * Gets a registered, healthy Anti Abuse Oracle endpoint and wallet. */ diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index 4aa5abecc75..26b9033968b 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -34,19 +34,7 @@ import { DiscoveryNode } from './types' import sample from 'lodash/sample' - -const getPathFromUrl = (url: string) => { - const pathRegex = /^([a-z]+:\/\/)?(?:www\.)?([^/]+)?(.*)$/ - - const match = url.match(pathRegex) - - if (match?.[3]) { - const path = match[3] - return path - } else { - throw new Error(`Invalid URL, couldn't get path.`) - } -} +import { getPathFromUrl } from '../../utils/getPathFromUrl' export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { /** diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts index 71f3a11ae64..3600b292649 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts @@ -20,13 +20,12 @@ export class SolanaRelay extends BaseAPI { */ private feePayer: PublicKey | null = null - constructor(config?: SolanaConfig) { + constructor(config: SolanaConfig) { super( new runtime.Configuration({ fetchApi: fetch, basePath: '/solana', - headers: { 'Content-Type': 'application/json' }, - middleware: config?.middleware + middleware: [config.discoveryNodeSelector.createMiddleware()] }) ) } diff --git a/packages/libs/src/sdk/services/Solana/types.ts b/packages/libs/src/sdk/services/Solana/types.ts index 068d3c675de..34adc9f71a2 100644 --- a/packages/libs/src/sdk/services/Solana/types.ts +++ b/packages/libs/src/sdk/services/Solana/types.ts @@ -5,22 +5,20 @@ import { Transaction } from '@solana/web3.js' import type { WalletAdapterProps } from '@solana/wallet-adapter-base' -import type * as runtime from '../../api/generated/default/runtime' import { z } from 'zod' import type { Prettify } from '../../utils/prettify' import type { SolanaRelay } from './SolanaRelay' +import { DiscoveryNodeSelectorService } from '../DiscoveryNodeSelector' -export type SolanaConfigInternal = { +export type SolanaWalletAdapter = WalletAdapterProps + +export type SolanaConfig = { /** - * Middleware for HTTP requests to the Solana relay service. + * Selector that finds a healthy discovery node. */ - middleware?: runtime.Middleware[] + discoveryNodeSelector: DiscoveryNodeSelectorService } -export type SolanaWalletAdapter = WalletAdapterProps - -export type SolanaConfig = Partial - export type SolanaRelayService = SolanaRelay export const MintSchema = z.enum(['wAUDIO', 'USDC']).default('wAUDIO') diff --git a/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts b/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts index ec2741e3a40..e449579b24c 100644 --- a/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts @@ -51,7 +51,9 @@ const mswHandlers = [ version: '1.2.3', block_difference: 0, network: { - discovery_nodes: [{ endpoint: discoveryNode, delegateOwnerWallet: '' }], + discovery_nodes_with_owner: [ + { endpoint: discoveryNode, delegateOwnerWallet: '' } + ], content_nodes: [storageNodeA, storageNodeB] } } diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index ede5fda0b3a..87c5885b532 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -13,6 +13,7 @@ import type { SolanaWalletAdapter } from './services/Solana' import type { AntiAbuseOracleSelectorService } from './services/AntiAbuseOracleSelector/types' +import { AntiAbuseOracleService } from './services/AntiAbuseOracle/types' export type ServicesContainer = { /** @@ -69,6 +70,11 @@ export type ServicesContainer = { * Service used to choose a healthy Anti Abuse Oracle */ antiAbuseOracleSelector: AntiAbuseOracleSelectorService + + /** + * Service used to interact with Anti Abuse Oracle + */ + antiAbuseOracle: AntiAbuseOracleService } const DevAppSchema = z.object({ diff --git a/packages/libs/src/sdk/utils/getPathFromUrl.ts b/packages/libs/src/sdk/utils/getPathFromUrl.ts new file mode 100644 index 00000000000..1427e276c40 --- /dev/null +++ b/packages/libs/src/sdk/utils/getPathFromUrl.ts @@ -0,0 +1,15 @@ +/** + * Extracts the path of a URL with query parameters. Platform agnostic. + */ +export const getPathFromUrl = (url: string) => { + const pathRegex = /^([a-z]+:\/\/)?(?:www\.)?([^/]+)?(.*)$/ + + const match = url.match(pathRegex) + + if (match?.[3]) { + const path = match[3] + return path + } else { + throw new Error(`Invalid URL, couldn't get path.`) + } +} From 30f135b556e44e348d16999c89299138c5736596 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:30:38 -0800 Subject: [PATCH 55/66] comments --- .../libs/src/sdk/api/challenges/ChallengesApi.ts | 10 ++++++---- .../ClaimableTokensClient.ts | 15 +++++++++++---- .../RewardManagerClient/RewardManagerClient.ts | 7 +++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts index 4fb11fdc99a..d05035ba975 100644 --- a/packages/libs/src/sdk/api/challenges/ChallengesApi.ts +++ b/packages/libs/src/sdk/api/challenges/ChallengesApi.ts @@ -1,9 +1,13 @@ import { wAUDIO } from '@audius/fixed-decimal' +import type { PublicKey } from '@solana/web3.js' +import { toChecksumAddress } from 'ethereumjs-util' + import type { ClaimableTokensClient, DiscoveryNodeSelectorService, LoggerService } from '../../services' +import { AntiAbuseOracleService } from '../../services/AntiAbuseOracle/types' import type { RewardManagerClient } from '../../services/Solana/programs/RewardManagerClient/RewardManagerClient' import { parseParams } from '../../utils/parseParams' import { BaseAPI, Configuration } from '../generated/default' @@ -12,6 +16,7 @@ import { Configuration as ConfigurationFull } from '../generated/full' import type { UsersApi } from '../users/UsersApi' + import { ChallengeId, ClaimRewardsRequest, @@ -19,9 +24,6 @@ import { GenerateSpecifierRequest, GenerateSpecifierSchema } from './types' -import type { PublicKey } from '@solana/web3.js' -import { AntiAbuseOracleService } from '../../services/AntiAbuseOracle/types' -import { toChecksumAddress } from 'ethereumjs-util' export class ChallengesApi extends BaseAPI { private readonly logger: LoggerService @@ -94,7 +96,7 @@ export class ChallengesApi extends BaseAPI { throw new Error(`Failed to find user ${args.userId}`) } const { ercWallet: recipientEthAddress, handle } = data - let attestationTransactionSignatures: string[] = [] + const attestationTransactionSignatures: string[] = [] this.logger.debug('Creating user bank if necessary...') const { userBank: destinationUserBank } = diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts index c5122d8d9d7..161395212f1 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts @@ -6,9 +6,13 @@ import { Secp256k1Program, PublicKey } from '@solana/web3.js' + +import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' import { parseParams } from '../../../../utils/parseParams' import type { Mint } from '../../types' +import { BaseSolanaProgram } from '../BaseSolanaProgram' +import { defaultClaimableTokensConfig } from './constants' import { type GetOrCreateUserBankRequest, GetOrCreateUserBankSchema, @@ -20,10 +24,13 @@ import { ClaimableTokensConfig } from './types' -import { BaseSolanaProgram } from '../BaseSolanaProgram' -import { defaultClaimableTokensConfig } from './constants' -import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' - +/** + * Connected client to the ClaimableTokens Solana program. + * + * The ClaimableTokens program is responsible for creation of program-owned + * associated token accounts that are permissioned to users by their Ethereum + * hedgehog wallet private keys. + */ export class ClaimableTokensClient extends BaseSolanaProgram { /** The program ID of the ClaimableTokensProgram instance. */ private readonly programId: PublicKey diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts index 554b707301b..fe18762a559 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts @@ -19,6 +19,13 @@ import type { RewardManagerStateData } from '@audius/spl/dist/types/reward-manag import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' import { defaultRewardManagerClentConfig } from './constants' +/** + * Connected client to the Solana RewardManager program. + * + * The RewardManager program is in charge of disbursing the community awards + * based on attestations from N uniquely owned discovery nodes and an anti abuse + * oracle node. + */ export class RewardManagerClient extends BaseSolanaProgram { private readonly programId: PublicKey private readonly rewardManagerStateAccount: PublicKey From 3a46392470321cf8d37bdc42cdd001addcfe2d66 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:28:09 -0800 Subject: [PATCH 56/66] lint fix --- .vscode/settings.json | 4 +-- packages/libs/src/sdk/api/challenges/types.ts | 2 ++ .../libs/src/sdk/api/users/UsersApi.test.ts | 26 +++++++++++-------- packages/libs/src/sdk/api/users/UsersApi.ts | 2 +- .../AntiAbuseOracle/AntiAbuseOracle.ts | 4 ++- .../src/sdk/services/AntiAbuseOracle/types.ts | 4 ++- .../AntiAbuseOracleSelector.test.ts | 3 ++- .../AntiAbuseOracleSelector.ts | 21 ++++++++------- .../AntiAbuseOracleSelector/constants.ts | 1 + .../DiscoveryNodeSelector.test.ts | 7 +++-- .../DiscoveryNodeSelector.ts | 4 +-- .../DiscoveryNodeSelector/healthCheckTypes.ts | 1 + .../src/sdk/services/Solana/SolanaRelay.ts | 10 ++++--- .../Solana/SolanaRelayWalletAdapter.ts | 3 +++ .../Solana/programs/BaseSolanaProgram.ts | 6 +++-- .../ClaimableTokensClient/constants.ts | 1 + .../programs/ClaimableTokensClient/types.ts | 5 ++-- .../RewardManagerClient.ts | 12 +++++---- .../programs/RewardManagerClient/constants.ts | 1 + .../programs/RewardManagerClient/types.ts | 3 ++- .../src/sdk/services/Solana/programs/types.ts | 1 + .../libs/src/sdk/services/Solana/types.ts | 6 +++-- packages/libs/src/sdk/types.ts | 8 +++--- 23 files changed, 82 insertions(+), 53 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f4af686f1b9..ceefbc3fd9c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,14 +21,14 @@ "python.testing.unittestEnabled": false, "[python]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "ms-python.black-formatter" }, "editor.find.addExtraSpaceOnTop": false, "eslint.workingDirectories": [{ "pattern": "./packages/*/" }], "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "gitlens.advanced.fileHistoryFollowsRenames": true } diff --git a/packages/libs/src/sdk/api/challenges/types.ts b/packages/libs/src/sdk/api/challenges/types.ts index 4af10c3e1e9..dc3ea6f3e61 100644 --- a/packages/libs/src/sdk/api/challenges/types.ts +++ b/packages/libs/src/sdk/api/challenges/types.ts @@ -1,7 +1,9 @@ import { z } from 'zod' + import { HashId } from '../../types/HashId' // @ts-ignore:next-line ignore the unused import, used in jsdoc +// eslint-disable-next-line @typescript-eslint/no-unused-vars import type { ChallengesApi } from './ChallengesApi' export enum ChallengeId { diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index 72f671857e7..a7b2151a43a 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -1,17 +1,8 @@ import fs from 'fs' import path from 'path' +import { ClaimableTokensProgram } from '@audius/spl' import { beforeAll, expect, jest } from '@jest/globals' - -import { Auth } from '../../services/Auth/Auth' -import { DiscoveryNodeSelector } from '../../services/DiscoveryNodeSelector' -import { EntityManager } from '../../services/EntityManager' -import { Logger } from '../../services/Logger' -import { Storage } from '../../services/Storage' -import { StorageNodeSelector } from '../../services/StorageNodeSelector' -import { Configuration } from '../generated/default' - -import { UsersApi } from './UsersApi' import { PublicKey, Secp256k1Program, @@ -19,12 +10,21 @@ import { TransactionMessage, VersionedTransaction } from '@solana/web3.js' -import { ClaimableTokensProgram } from '@audius/spl' + import { ClaimableTokensClient, SolanaRelay, SolanaRelayWalletAdapter } from '../../services' +import { Auth } from '../../services/Auth/Auth' +import { DiscoveryNodeSelector } from '../../services/DiscoveryNodeSelector' +import { EntityManager } from '../../services/EntityManager' +import { Logger } from '../../services/Logger' +import { Storage } from '../../services/Storage' +import { StorageNodeSelector } from '../../services/StorageNodeSelector' +import { Configuration } from '../generated/default' + +import { UsersApi } from './UsersApi' const pngFile = fs.readFileSync( path.resolve(__dirname, '../../test/png-file.png') @@ -288,18 +288,22 @@ describe('UsersApi', () => { ) // Typescript hint - always true if (ClaimableTokensProgram.isTransferInstruction(decoded)) { + // eslint-disable-next-line jest/no-conditional-expect expect(decoded.keys.destination.pubkey.toBase58()).toBe( userBanks[receiverUserId]?.toBase58() ) + // eslint-disable-next-line jest/no-conditional-expect expect(decoded.keys.sourceUserBank.pubkey.toBase58()).toBe( userBanks[senderUserId]?.toBase58() ) const data = ClaimableTokensProgram.decodeSignedTransferInstructionData(secp!) + // eslint-disable-next-line jest/no-conditional-expect expect(data.destination.toBase58()).toBe( userBanks[receiverUserId]?.toBase58() ) + // eslint-disable-next-line jest/no-conditional-expect expect(data.amount).toBe(outputAmount) } return { signature: 'fake-sig' } diff --git a/packages/libs/src/sdk/api/users/UsersApi.ts b/packages/libs/src/sdk/api/users/UsersApi.ts index 66c68fd5725..d8822851d7b 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.ts @@ -12,6 +12,7 @@ import { AdvancedOptions } from '../../services/EntityManager/types' import type { LoggerService } from '../../services/Logger' +import type { ClaimableTokensClient } from '../../services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient' import { parseParams } from '../../utils/parseParams' import { retry3 } from '../../utils/retry' import { @@ -38,7 +39,6 @@ import { SendTipRequest, SendTipSchema } from './types' -import type { ClaimableTokensClient } from '../../services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient' export class UsersApi extends GeneratedUsersApi { constructor( diff --git a/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts b/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts index 16e6a575ff1..c35d7d880bf 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracle/AntiAbuseOracle.ts @@ -1,7 +1,10 @@ +import fetch from 'cross-fetch' + import { BaseAPI, JSONApiResponse, exists } from '../../api/generated/default' import * as runtime from '../../api/generated/default/runtime' import { parseParams } from '../../utils/parseParams' import { AntiAbuseOracleSelectorService } from '../AntiAbuseOracleSelector' + import { AntiAbuseOracleConfig, AntiAbuseOracleService, @@ -9,7 +12,6 @@ import { AttestationResponse, GetAttestationSchema } from './types' -import fetch from 'cross-fetch' export class AntiAbuseOracle extends BaseAPI implements AntiAbuseOracleService { private readonly antiAbuseOracleSelector: AntiAbuseOracleSelectorService diff --git a/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts b/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts index 51d02f8aa63..eaf31878850 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracle/types.ts @@ -1,7 +1,9 @@ import { z } from 'zod' -import { ChallengeId } from '../../api/challenges/types' + // @ts-ignore:next-line ignore the unused import, used in jsdoc +// eslint-disable-next-line @typescript-eslint/no-unused-vars import type { ChallengesApi } from '../../api/challenges/ChallengesApi' +import { ChallengeId } from '../../api/challenges/types' import { AntiAbuseOracleSelectorService } from '../AntiAbuseOracleSelector' export const GetAttestationSchema = z.object({ diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts index 717c4aa6b60..5f292ef12f7 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.test.ts @@ -1,7 +1,8 @@ import { rest } from 'msw' -import { AntiAbuseOracleSelector } from './AntiAbuseOracleSelector' import { setupServer } from 'msw/node' +import { AntiAbuseOracleSelector } from './AntiAbuseOracleSelector' + const HEALTHY_NODE = 'https://healthy-aao.audius.co' const OFFLINE_NODE = 'https://offline-aao.audius.co' const UNHEALTHY_NODE = 'https://unhealthy-aao.audius.co' diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts index d2be47e2bc8..ab14e77672f 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/AntiAbuseOracleSelector.ts @@ -1,21 +1,22 @@ -import sample from 'lodash/sample' +import fetch from 'cross-fetch' + +import { + ErrorContext, + Middleware, + RequestContext +} from '../../api/generated/default' +import { getPathFromUrl } from '../../utils/getPathFromUrl' import { mergeConfigWithDefaults } from '../../utils/mergeConfigs' +import { promiseAny } from '../../utils/promiseAny' +import { AntiAbuseOracleHealthCheckResponse } from '../AntiAbuseOracle/types' import type { LoggerService } from '../Logger' + import { defaultAntiAbuseOracleSelectorConfig } from './constants' import type { AntiAbuseOracleSelectorService, AntiAbuseOracleNode, AntiAbuseOracleSelectorConfig } from './types' -import { - ErrorContext, - Middleware, - RequestContext -} from '../../api/generated/default' -import { getPathFromUrl } from '../../utils/getPathFromUrl' -import { AntiAbuseOracleHealthCheckResponse } from '../AntiAbuseOracle/types' -import fetch from 'cross-fetch' -import { promiseAny } from '../../utils/promiseAny' export class AntiAbuseOracleSelector implements AntiAbuseOracleSelectorService { private readonly endpoints: string[] diff --git a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts index 68e48f62bf8..85d51a9a208 100644 --- a/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts +++ b/packages/libs/src/sdk/services/AntiAbuseOracleSelector/constants.ts @@ -1,5 +1,6 @@ import { productionConfig } from '../../config' import { Logger } from '../Logger' + import type { AntiAbuseOracleSelectorConfigInternal } from './types' export const defaultAntiAbuseOracleSelectorConfig: AntiAbuseOracleSelectorConfigInternal = diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts index 1e4ab6fbc5c..885d58d7a2c 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts @@ -1,4 +1,5 @@ import fetch, { Response } from 'cross-fetch' +import shuffle from 'lodash/shuffle' import { rest } from 'msw' import { setupServer } from 'msw/node' @@ -10,7 +11,6 @@ import type { HealthCheckResponseData } from './healthCheckTypes' import type { DiscoveryNode } from './types' -import shuffle from 'lodash/shuffle' // jest.mock('./healthChecks', () => ({ // getHealthCheck: jest.fn(() => ({})) @@ -786,7 +786,7 @@ describe('discoveryNodeSelector', () => { }) const nodes = await selector.getUniquelyOwnedEndpoints(3) expect(nodes.length).toBe(3) - expect(nodes.every((n) => n === HEALTHY_NODE)) + expect(nodes.every((n) => n === HEALTHY_NODE)).toBe(true) }) test('filters to allowlist', async () => { @@ -845,8 +845,7 @@ describe('discoveryNodeSelector', () => { }) await expect(async () => { - const res = await selector.getUniquelyOwnedEndpoints(3) - console.log(res) + await selector.getUniquelyOwnedEndpoints(3) }).rejects.toThrow( new Error('Not enough healthy services to choose from') ) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index 26b9033968b..43562b1c992 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -1,5 +1,6 @@ import EventEmitter from 'events' +import sample from 'lodash/sample' import sampleSize from 'lodash/sampleSize' import { AbortController as AbortControllerPolyfill } from 'node-abort-controller' import semver from 'semver' @@ -11,6 +12,7 @@ import type { RequestContext, ResponseContext } from '../../api/generated/default' +import { getPathFromUrl } from '../../utils/getPathFromUrl' import { mergeConfigWithDefaults } from '../../utils/mergeConfigs' import { promiseAny } from '../../utils/promiseAny' import type { LoggerService } from '../Logger' @@ -33,8 +35,6 @@ import { ServiceSelectionEvents, DiscoveryNode } from './types' -import sample from 'lodash/sample' -import { getPathFromUrl } from '../../utils/getPathFromUrl' export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { /** diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts index 76a8d57491b..d74650ab1b1 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthCheckTypes.ts @@ -1,6 +1,7 @@ import type { CommsResponse } from '../../api/chats/serverTypes' import type { DeepPartial } from '../../utils/deepPartial' import type { StorageNode } from '../StorageNodeSelector' + import type { DiscoveryNode } from './types' export type FlaskFullResponse = Partial<{ diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts index 3600b292649..454e406cd73 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts @@ -1,14 +1,16 @@ import { PublicKey } from '@solana/web3.js' +import fetch from 'cross-fetch' + import { BaseAPI } from '../../api/generated/default' import * as runtime from '../../api/generated/default/runtime' import { parseParams } from '../../utils/parseParams' + import { type RelayRequestBody, type SolanaConfig, RelayRequest, RelaySchema } from './types' -import fetch from 'cross-fetch' /** * Client for the Solana Relay Plugin on Discovery. @@ -56,7 +58,7 @@ export class SolanaRelay extends BaseAPI { (json) => ({ feePayer: !runtime.exists(json, 'feePayer') ? undefined - : new PublicKey(json['feePayer'] as string) + : new PublicKey(json.feePayer as string) }) ).value() if (!feePayer) { @@ -90,7 +92,7 @@ export class SolanaRelay extends BaseAPI { path: '/relay', method: 'POST', headers: headerParameters, - body: body + body }, initOverrides ) @@ -100,7 +102,7 @@ export class SolanaRelay extends BaseAPI { throw new Error('Signature missing') } return { - signature: json['signature'] as string + signature: json.signature as string } }).value() } diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts index 5c8226a2b97..38b8f37e809 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts @@ -5,6 +5,7 @@ import type { WalletReadyState } from '@solana/wallet-adapter-base' import type { PublicKey } from '@solana/web3.js' + import type { SolanaRelay } from './SolanaRelay' import type { SolanaWalletAdapter } from './types' @@ -21,10 +22,12 @@ import type { SolanaWalletAdapter } from './types' export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { public readonly name = 'AudiusSolanaWallet' as WalletName<'AudiusSolanaWallet'> + public readonly url = '' public readonly icon = '' public readonly readyState: WalletReadyState = 'Loadable' as WalletReadyState.Loadable + public readonly supportedTransactionVersions?: SupportedTransactionVersions private readonly solanaRelay: SolanaRelay diff --git a/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts b/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts index 8a5f4a3f42e..2cdb1645dcf 100644 --- a/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts +++ b/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts @@ -5,13 +5,15 @@ import { TransactionMessage, VersionedTransaction } from '@solana/web3.js' + +import { parseParams } from '../../../utils/parseParams' import type { SolanaWalletAdapter } from '../types' + import { BuildTransactionRequest, BuildTransactionSchema, type BaseSolanaProgramConfigInternal } from './types' -import { parseParams } from '../../../utils/parseParams' const isPublicKeyArray = (arr: any[]): arr is PublicKey[] => arr.every((a) => a instanceof PublicKey) @@ -96,7 +98,7 @@ export class BaseSolanaProgram { recentBlockhash = res.blockhash } - let addressLookupTableAccounts = !isPublicKeyArray(addressLookupTables) + const addressLookupTableAccounts = !isPublicKeyArray(addressLookupTables) ? addressLookupTables : await this.getLookupTableAccounts(addressLookupTables) diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/constants.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/constants.ts index 03bb2d15653..4075eda44c3 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/constants.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/constants.ts @@ -1,4 +1,5 @@ import { PublicKey } from '@solana/web3.js' + import type { ClaimableTokensConfigInternal } from './types' export const defaultClaimableTokensConfig: ClaimableTokensConfigInternal = { diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts index e1a1135086c..807e19ad524 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/types.ts @@ -1,4 +1,7 @@ +import type { PublicKey } from '@solana/web3.js' import { z } from 'zod' + +import type { Prettify } from '../../../../utils/prettify' import type { AuthService } from '../../../Auth' import { Mint, @@ -6,8 +9,6 @@ import { PublicKeySchema, SolanaWalletAdapter } from '../../types' -import type { PublicKey } from '@solana/web3.js' -import type { Prettify } from '../../../../utils/prettify' import type { BaseSolanaProgramConfigInternal } from '../types' export type ClaimableTokensConfigInternal = { diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts index fe18762a559..beb24ae44b6 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts @@ -1,5 +1,12 @@ import { RewardManagerProgram } from '@audius/spl' +import type { RewardManagerStateData } from '@audius/spl/dist/types/reward-manager/types' +import { Secp256k1Program, type PublicKey } from '@solana/web3.js' + +import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' +import { parseParams } from '../../../../utils/parseParams' import { BaseSolanaProgram } from '../BaseSolanaProgram' + +import { defaultRewardManagerClentConfig } from './constants' import { CreateEvaluateAttestationsInstructionRequest, CreateEvaluateAttestationsInstructionSchema, @@ -13,11 +20,6 @@ import { GetSubmittedAttestationsRequest, GetSubmittedAttestationsSchema } from './types' -import { Secp256k1Program, type PublicKey } from '@solana/web3.js' -import { parseParams } from '../../../../utils/parseParams' -import type { RewardManagerStateData } from '@audius/spl/dist/types/reward-manager/types' -import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' -import { defaultRewardManagerClentConfig } from './constants' /** * Connected client to the Solana RewardManager program. diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts index 742ab052fdf..b04011b0c08 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/constants.ts @@ -1,4 +1,5 @@ import { PublicKey } from '@solana/web3.js' + import type { RewardManagerClientConfigInternal } from './types' export const defaultRewardManagerClentConfig: RewardManagerClientConfigInternal = diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts index 54fe5e89bd8..fefb6344ee5 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/types.ts @@ -1,7 +1,8 @@ import type { PublicKey } from '@solana/web3.js' -import type { BaseSolanaProgramConfigInternal } from '../types' import { z } from 'zod' + import { PublicKeySchema, SolanaWalletAdapter } from '../../types' +import type { BaseSolanaProgramConfigInternal } from '../types' export type RewardManagerClientConfigInternal = { programId: PublicKey diff --git a/packages/libs/src/sdk/services/Solana/programs/types.ts b/packages/libs/src/sdk/services/Solana/programs/types.ts index c6a1fcac4da..1210ed3c9c3 100644 --- a/packages/libs/src/sdk/services/Solana/programs/types.ts +++ b/packages/libs/src/sdk/services/Solana/programs/types.ts @@ -4,6 +4,7 @@ import { type ConnectionConfig } from '@solana/web3.js' import { z } from 'zod' + import { PublicKeySchema } from '../types' export type BaseSolanaProgramConfigInternal = { diff --git a/packages/libs/src/sdk/services/Solana/types.ts b/packages/libs/src/sdk/services/Solana/types.ts index 34adc9f71a2..1aea1c69a39 100644 --- a/packages/libs/src/sdk/services/Solana/types.ts +++ b/packages/libs/src/sdk/services/Solana/types.ts @@ -1,15 +1,17 @@ +import type { WalletAdapterProps } from '@solana/wallet-adapter-base' import { PublicKey, VersionedTransaction, SendOptions, Transaction } from '@solana/web3.js' -import type { WalletAdapterProps } from '@solana/wallet-adapter-base' import { z } from 'zod' + import type { Prettify } from '../../utils/prettify' -import type { SolanaRelay } from './SolanaRelay' import { DiscoveryNodeSelectorService } from '../DiscoveryNodeSelector' +import type { SolanaRelay } from './SolanaRelay' + export type SolanaWalletAdapter = WalletAdapterProps export type SolanaConfig = { diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index 87c5885b532..2842d3156aa 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -1,19 +1,19 @@ import { z } from 'zod' +import { AntiAbuseOracleService } from './services/AntiAbuseOracle/types' +import type { AntiAbuseOracleSelectorService } from './services/AntiAbuseOracleSelector/types' import type { AuthService } from './services/Auth' import type { DiscoveryNodeSelectorService } from './services/DiscoveryNodeSelector' import type { EntityManagerService } from './services/EntityManager' import type { LoggerService } from './services/Logger' -import type { StorageService } from './services/Storage' -import type { StorageNodeSelectorService } from './services/StorageNodeSelector' import type { RewardManagerClient, ClaimableTokensClient, SolanaRelayService, SolanaWalletAdapter } from './services/Solana' -import type { AntiAbuseOracleSelectorService } from './services/AntiAbuseOracleSelector/types' -import { AntiAbuseOracleService } from './services/AntiAbuseOracle/types' +import type { StorageService } from './services/Storage' +import type { StorageNodeSelectorService } from './services/StorageNodeSelector' export type ServicesContainer = { /** From db1273f80b3c80d7ebf147ccc9be9896bd3d580b Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:29:59 -0800 Subject: [PATCH 57/66] meh types --- .../libs/src/sdk/api/users/UsersApi.test.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index a7b2151a43a..fde2373550b 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -2,6 +2,7 @@ import fs from 'fs' import path from 'path' import { ClaimableTokensProgram } from '@audius/spl' +import { DecodedTransferClaimableTokensInstruction } from '@audius/spl/dist/types/claimable-tokens/types' import { beforeAll, expect, jest } from '@jest/globals' import { PublicKey, @@ -286,26 +287,23 @@ describe('UsersApi', () => { expect(ClaimableTokensProgram.isTransferInstruction(decoded)).toBe( true ) - // Typescript hint - always true - if (ClaimableTokensProgram.isTransferInstruction(decoded)) { - // eslint-disable-next-line jest/no-conditional-expect - expect(decoded.keys.destination.pubkey.toBase58()).toBe( - userBanks[receiverUserId]?.toBase58() - ) - // eslint-disable-next-line jest/no-conditional-expect - expect(decoded.keys.sourceUserBank.pubkey.toBase58()).toBe( - userBanks[senderUserId]?.toBase58() - ) - const data = - ClaimableTokensProgram.decodeSignedTransferInstructionData(secp!) - - // eslint-disable-next-line jest/no-conditional-expect - expect(data.destination.toBase58()).toBe( - userBanks[receiverUserId]?.toBase58() - ) - // eslint-disable-next-line jest/no-conditional-expect - expect(data.amount).toBe(outputAmount) - } + // Typescript hint - see above assert + const decoded2 = decoded as DecodedTransferClaimableTokensInstruction + + expect(decoded2.keys.destination.pubkey.toBase58()).toBe( + userBanks[receiverUserId]?.toBase58() + ) + expect(decoded2.keys.sourceUserBank.pubkey.toBase58()).toBe( + userBanks[senderUserId]?.toBase58() + ) + const data = + ClaimableTokensProgram.decodeSignedTransferInstructionData(secp!) + + expect(data.destination.toBase58()).toBe( + userBanks[receiverUserId]?.toBase58() + ) + expect(data.amount).toBe(outputAmount) + return { signature: 'fake-sig' } }) From 22b4d696657cc315740e40a61ee0edf2a1817d41 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:45:17 -0800 Subject: [PATCH 58/66] fix lint, program=>client --- .../libs/src/sdk/api/users/UsersApi.test.ts | 2 +- packages/libs/src/sdk/sdk.ts | 42 +++++++++++-------- .../src/sdk/services/Solana/SolanaRelay.ts | 18 +------- packages/libs/src/sdk/types.ts | 4 +- .../mobile/src/services/sdk/audius-sdk.ts | 8 ++-- packages/mobile/src/services/sdk/solana.ts | 4 +- .../web/src/services/audius-sdk/audiusSdk.ts | 6 +-- .../web/src/services/audius-sdk/solana.ts | 23 +++++----- 8 files changed, 50 insertions(+), 57 deletions(-) diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index fde2373550b..11fbe06e484 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -68,7 +68,7 @@ describe('UsersApi', () => { discoveryNodeSelector, logger }) - const solanaRelay = new SolanaRelay({ discoveryNodeSelector }) + const solanaRelay = new SolanaRelay() const claimableTokens = new ClaimableTokensClient({ solanaWalletAdapter: new SolanaRelayWalletAdapter({ solanaRelay }) }) diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index 56c8c42e19f..9f9f1130a06 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -114,25 +114,31 @@ const initializeServices = (config: SdkConfig) => { config.services?.antiAbuseOracleSelector ?? new AntiAbuseOracleSelector({ logger }) - const defaultSolanaRelay = new SolanaRelay({ - discoveryNodeSelector - }) + const defaultSolanaRelay = new SolanaRelay( + new Configuration({ + middleware: [discoveryNodeSelector.createMiddleware()] + }) + ) const defaultSolanaWalletAdapter = new SolanaRelayWalletAdapter({ solanaRelay: config.services?.solanaRelay ?? defaultSolanaRelay }) - const defaultClaimableTokensProgram = new ClaimableTokensClient({ - ...defaultClaimableTokensConfig, - solanaWalletAdapter: - config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter - }) + const claimableTokensClient = + config.services?.claimableTokensClient ?? + new ClaimableTokensClient({ + ...defaultClaimableTokensConfig, + solanaWalletAdapter: + config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter + }) - const defaultRewardManagerProgram = new RewardManagerClient({ - ...defaultRewardManagerClentConfig, - solanaWalletAdapter: - config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter - }) + const rewardManagerClient = + config.services?.rewardManagerClient ?? + new RewardManagerClient({ + ...defaultRewardManagerClentConfig, + solanaWalletAdapter: + config.services?.solanaWalletAdapter ?? defaultSolanaWalletAdapter + }) const defaultAntiAbuseOracle = new AntiAbuseOracle({ antiAbuseOracleSelector @@ -145,8 +151,8 @@ const initializeServices = (config: SdkConfig) => { entityManager: defaultEntityManager, storage: defaultStorage, auth: defaultAuthService, - claimableTokensProgram: defaultClaimableTokensProgram, - rewardManagerProgram: defaultRewardManagerProgram, + claimableTokensClient, + rewardManagerClient, solanaWalletAdapter: defaultSolanaWalletAdapter, solanaRelay: defaultSolanaRelay, antiAbuseOracle: defaultAntiAbuseOracle, @@ -186,7 +192,7 @@ const initializeApis = ({ services.entityManager, services.auth, services.logger, - services.claimableTokensProgram + services.claimableTokensClient ) const albums = new AlbumsApi( generatedApiClientConfig, @@ -236,8 +242,8 @@ const initializeApis = ({ generatedApiClientConfig, users, services.discoveryNodeSelector, - services.rewardManagerProgram, - services.claimableTokensProgram, + services.rewardManagerClient, + services.claimableTokensClient, services.antiAbuseOracle, services.logger ) diff --git a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts index 454e406cd73..c6eec7c51db 100644 --- a/packages/libs/src/sdk/services/Solana/SolanaRelay.ts +++ b/packages/libs/src/sdk/services/Solana/SolanaRelay.ts @@ -1,16 +1,10 @@ import { PublicKey } from '@solana/web3.js' -import fetch from 'cross-fetch' import { BaseAPI } from '../../api/generated/default' import * as runtime from '../../api/generated/default/runtime' import { parseParams } from '../../utils/parseParams' -import { - type RelayRequestBody, - type SolanaConfig, - RelayRequest, - RelaySchema -} from './types' +import { type RelayRequestBody, RelayRequest, RelaySchema } from './types' /** * Client for the Solana Relay Plugin on Discovery. @@ -22,16 +16,6 @@ export class SolanaRelay extends BaseAPI { */ private feePayer: PublicKey | null = null - constructor(config: SolanaConfig) { - super( - new runtime.Configuration({ - fetchApi: fetch, - basePath: '/solana', - middleware: [config.discoveryNodeSelector.createMiddleware()] - }) - ) - } - /** * Gets a random fee payer public key from the selected discovery node's * Solana relay plugin. diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index 2842d3156aa..4c470ff5672 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -59,12 +59,12 @@ export type ServicesContainer = { /** * Claimable Tokens Program client for Solana */ - claimableTokensProgram: ClaimableTokensClient + claimableTokensClient: ClaimableTokensClient /** * Reward Manager Program client for Solana */ - rewardManagerProgram: RewardManagerClient + rewardManagerClient: RewardManagerClient /** * Service used to choose a healthy Anti Abuse Oracle diff --git a/packages/mobile/src/services/sdk/audius-sdk.ts b/packages/mobile/src/services/sdk/audius-sdk.ts index 5422e55c56a..7d45ce0d12e 100644 --- a/packages/mobile/src/services/sdk/audius-sdk.ts +++ b/packages/mobile/src/services/sdk/audius-sdk.ts @@ -24,11 +24,11 @@ const initSdk = async () => { discoveryNodeSelector: await discoveryNodeSelectorService.getInstance(), auth, storageNodeSelector: await getStorageNodeSelector(), - claimableTokensProgram: claimableTokensService, - rewardManagerProgram: rewardManagerService, + claimableTokensClient: claimableTokensService, + rewardManagerClient: rewardManagerService, antiAbuseOracleSelector: new AntiAbuseOracleSelector({ endpoints: [env.AAO_ENDPOINT], - addresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] + registeredAddresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] }) } }) @@ -43,7 +43,7 @@ export const audiusSdk = async () => { await new Promise((resolve) => { sdkEventEmitter.addListener(SDK_LOADED_EVENT_NAME, resolve) }) - return await sdkInstance + return sdkInstance } else if (!sdkInstance) { return await initSdk() } diff --git a/packages/mobile/src/services/sdk/solana.ts b/packages/mobile/src/services/sdk/solana.ts index 941e2d2e32b..1f3377c9050 100644 --- a/packages/mobile/src/services/sdk/solana.ts +++ b/packages/mobile/src/services/sdk/solana.ts @@ -8,7 +8,7 @@ import { PublicKey } from '@solana/web3.js' import { env } from '../env' -const solanaRelay = new SolanaRelay({ +const solanaRelay = new SolanaRelay(new Configuration({ middleware: [ { pre: async (context) => { @@ -18,7 +18,7 @@ const solanaRelay = new SolanaRelay({ } } ] -}) +})) const solanaWalletAdapter = new SolanaRelayWalletAdapter({ solanaRelay }) diff --git a/packages/web/src/services/audius-sdk/audiusSdk.ts b/packages/web/src/services/audius-sdk/audiusSdk.ts index b2fa7d7ed07..cba4877a5ff 100644 --- a/packages/web/src/services/audius-sdk/audiusSdk.ts +++ b/packages/web/src/services/audius-sdk/audiusSdk.ts @@ -41,11 +41,11 @@ const initSdk = async () => { entityManager: makeEntityManagerInstance(discoveryNodeSelector), auth, storageNodeSelector: await getStorageNodeSelector(), - claimableTokensProgram: claimableTokensService, - rewardManagerProgram: rewardManagerService, + claimableTokensClient: claimableTokensService, + rewardManagerClient: rewardManagerService, antiAbuseOracleSelector: new AntiAbuseOracleSelector({ endpoints: [env.AAO_ENDPOINT!], - addresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] + registeredAddresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] }) } }) diff --git a/packages/web/src/services/audius-sdk/solana.ts b/packages/web/src/services/audius-sdk/solana.ts index 930361084d4..3b11b2f9b1f 100644 --- a/packages/web/src/services/audius-sdk/solana.ts +++ b/packages/web/src/services/audius-sdk/solana.ts @@ -1,22 +1,25 @@ import { ClaimableTokensClient, + Configuration, RewardManagerClient, SolanaRelay, SolanaRelayWalletAdapter } from '@audius/sdk' import { PublicKey } from '@solana/web3.js' -const solanaRelay = new SolanaRelay({ - middleware: [ - { - pre: async (context) => { - const endpoint = process.env.VITE_SOLANA_RELAY_ENDPOINT - const url = `${endpoint}${context.url}` - return { url, init: context.init } +const solanaRelay = new SolanaRelay( + new Configuration({ + middleware: [ + { + pre: async (context) => { + const endpoint = process.env.VITE_SOLANA_RELAY_ENDPOINT + const url = `${endpoint}${context.url}` + return { url, init: context.init } + } } - } - ] -}) + ] + }) +) const solanaWalletAdapter = new SolanaRelayWalletAdapter({ solanaRelay }) From d011c031bc490e79df6078eae197bf6e78af88ee Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 19 Jan 2024 18:06:25 -0800 Subject: [PATCH 59/66] fix mobile --- packages/mobile/src/services/sdk/solana.ts | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/mobile/src/services/sdk/solana.ts b/packages/mobile/src/services/sdk/solana.ts index 1f3377c9050..8f66a6da842 100644 --- a/packages/mobile/src/services/sdk/solana.ts +++ b/packages/mobile/src/services/sdk/solana.ts @@ -2,23 +2,26 @@ import { ClaimableTokensClient, RewardManagerClient, SolanaRelay, - SolanaRelayWalletAdapter + SolanaRelayWalletAdapter, + Configuration } from '@audius/sdk' import { PublicKey } from '@solana/web3.js' import { env } from '../env' -const solanaRelay = new SolanaRelay(new Configuration({ - middleware: [ - { - pre: async (context) => { - const endpoint = env.SOLANA_RELAY_ENDPOINT - const url = `${endpoint}${context.url}` - return { url, init: context.init } +const solanaRelay = new SolanaRelay( + new Configuration({ + middleware: [ + { + pre: async (context) => { + const endpoint = env.SOLANA_RELAY_ENDPOINT + const url = `${endpoint}${context.url}` + return { url, init: context.init } + } } - } - ] -})) + ] + }) +) const solanaWalletAdapter = new SolanaRelayWalletAdapter({ solanaRelay }) From 57e90e3cf434a22ad65b23b8c146dde2730d7806 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:33:48 -0800 Subject: [PATCH 60/66] Fix test --- .../DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts index 885d58d7a2c..dae408614fa 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts @@ -786,7 +786,9 @@ describe('discoveryNodeSelector', () => { }) const nodes = await selector.getUniquelyOwnedEndpoints(3) expect(nodes.length).toBe(3) - expect(nodes.every((n) => n === HEALTHY_NODE)).toBe(true) + expect(nodes).toContain(healthyNodes[0]) + expect(nodes).toContain(healthyNodes[1]) + expect(nodes).toContain(healthyNodes[2]) }) test('filters to allowlist', async () => { From 9ce4066b1e45c453b0cdb07373fd53fbe0911e56 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:49:28 -0800 Subject: [PATCH 61/66] Rename get_all_other_nodes.py --- packages/discovery-provider/src/api/v1/tracks.py | 2 +- .../queries/confirm_indexing_transaction_error.py | 14 +++++++++----- .../src/queries/get_attestation.py | 2 +- .../discovery-provider/src/queries/get_health.py | 6 +++--- .../src/queries/get_redirect_weights.py | 2 +- .../src/tasks/cache_current_nodes.py | 2 +- .../discovery-provider/src/tasks/index_metrics.py | 14 +++++++++----- .../src/tasks/update_clique_signers.py | 2 +- .../{get_all_other_nodes.py => get_all_nodes.py} | 12 ++++++------ 9 files changed, 32 insertions(+), 24 deletions(-) rename packages/discovery-provider/src/utils/{get_all_other_nodes.py => get_all_nodes.py} (93%) diff --git a/packages/discovery-provider/src/api/v1/tracks.py b/packages/discovery-provider/src/api/v1/tracks.py index 5b6b31ad01f..06e77fda75f 100644 --- a/packages/discovery-provider/src/api/v1/tracks.py +++ b/packages/discovery-provider/src/api/v1/tracks.py @@ -79,7 +79,7 @@ ) from src.trending_strategies.trending_type_and_version import TrendingType from src.utils import redis_connection -from src.utils.get_all_other_nodes import get_all_healthy_content_nodes_cached +from src.utils.get_all_nodes import get_all_healthy_content_nodes_cached from src.utils.redis_cache import cache from src.utils.redis_metrics import record_metrics from src.utils.rendezvous import RendezvousHash diff --git a/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py b/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py index 1927242dfff..28a268895f2 100644 --- a/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py +++ b/packages/discovery-provider/src/queries/confirm_indexing_transaction_error.py @@ -3,7 +3,8 @@ import requests from src.queries.get_skipped_transactions import set_indexing_error -from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached +from src.utils.config import shared_config +from src.utils.get_all_nodes import get_all_discovery_nodes_cached logger = logging.getLogger(__name__) @@ -19,13 +20,16 @@ def confirm_indexing_transaction_error( Gets all other discovery nodes and makes an api call to check the status of a transaction given a blocknumber, blockhash, and transactionhash """ - all_other_nodes = get_all_discovery_nodes_cached(redis) or [] - if not all_other_nodes: + all_nodes = get_all_discovery_nodes_cached(redis) or [] + if not all_nodes: return False - num_other_nodes = len(all_other_nodes) + num_other_nodes = len(all_nodes) - 1 num_transaction_failures = 0 - for node in all_other_nodes: + for node in all_nodes: + # Skip self + if node["delegateOwnerWallet"] == shared_config["delegate"]["owner_wallet"]: + continue try: endpoint = f"{node['endpoint']}/indexing/transaction_status?blocknumber={blocknumber}&blockhash={blockhash}&transactionhash={transactionhash}" response = requests.get(endpoint, timeout=10) diff --git a/packages/discovery-provider/src/queries/get_attestation.py b/packages/discovery-provider/src/queries/get_attestation.py index c53bb947bc7..8f7d2ab9d44 100644 --- a/packages/discovery-provider/src/queries/get_attestation.py +++ b/packages/discovery-provider/src/queries/get_attestation.py @@ -24,7 +24,7 @@ oracle_addresses_key, ) from src.utils.config import shared_config -from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached +from src.utils.get_all_nodes import get_all_discovery_nodes_cached from src.utils.redis_connection import get_redis REWARDS_MANAGER_ACCOUNT = shared_config["solana"]["rewards_manager_account"] diff --git a/packages/discovery-provider/src/queries/get_health.py b/packages/discovery-provider/src/queries/get_health.py index 32108267b7a..27b5408051f 100644 --- a/packages/discovery-provider/src/queries/get_health.py +++ b/packages/discovery-provider/src/queries/get_health.py @@ -25,7 +25,7 @@ from src.queries.get_trusted_notifier_discrepancies import get_delist_statuses_ok from src.utils import ( db_session, - get_all_other_nodes, + get_all_nodes, helpers, redis_connection, web3_provider, @@ -322,8 +322,8 @@ def get_health(args: GetHealthArgs, use_redis_cache: bool = True) -> Tuple[Dict, ) == "postgresql://postgres:postgres@db:5432/audius_discovery" or "localhost" in os.getenv( "audius_db_url", "" ) - discovery_nodes = get_all_other_nodes.get_all_discovery_nodes_cached(redis) - content_nodes = get_all_other_nodes.get_all_healthy_content_nodes_cached(redis) + discovery_nodes = get_all_nodes.get_all_discovery_nodes_cached(redis) + content_nodes = get_all_nodes.get_all_healthy_content_nodes_cached(redis) final_poa_block = helpers.get_final_poa_block() health_results = { "web": { diff --git a/packages/discovery-provider/src/queries/get_redirect_weights.py b/packages/discovery-provider/src/queries/get_redirect_weights.py index 9d0d3431c05..53f2ddf7dcd 100644 --- a/packages/discovery-provider/src/queries/get_redirect_weights.py +++ b/packages/discovery-provider/src/queries/get_redirect_weights.py @@ -4,7 +4,7 @@ from flask import Blueprint from src.api_helpers import success_response -from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached +from src.utils.get_all_nodes import get_all_discovery_nodes_cached from src.utils.redis_cache import cache, internal_api_cache_prefix from src.utils.redis_connection import get_redis diff --git a/packages/discovery-provider/src/tasks/cache_current_nodes.py b/packages/discovery-provider/src/tasks/cache_current_nodes.py index db428591fee..b3fa7ddb92a 100644 --- a/packages/discovery-provider/src/tasks/cache_current_nodes.py +++ b/packages/discovery-provider/src/tasks/cache_current_nodes.py @@ -1,7 +1,7 @@ import logging from src.tasks.celery_app import celery -from src.utils.get_all_other_nodes import ( +from src.utils.get_all_nodes import ( ALL_CONTENT_NODES_CACHE_KEY, ALL_DISCOVERY_NODES_CACHE_KEY, ALL_HEALTHY_CONTENT_NODES_CACHE_KEY, diff --git a/packages/discovery-provider/src/tasks/index_metrics.py b/packages/discovery-provider/src/tasks/index_metrics.py index 2cd9b5efd77..5b88e564f49 100644 --- a/packages/discovery-provider/src/tasks/index_metrics.py +++ b/packages/discovery-provider/src/tasks/index_metrics.py @@ -11,7 +11,8 @@ update_historical_monthly_route_metrics, ) from src.tasks.celery_app import celery -from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached +from src.utils.config import shared_config +from src.utils.get_all_nodes import get_all_discovery_nodes_cached from src.utils.prometheus_metric import ( PrometheusMetric, PrometheusMetricNames, @@ -76,7 +77,7 @@ def consolidate_metrics_from_other_nodes(self, db, redis): and merge with this node's metrics so that this node will be aware of all the metrics across users hitting different providers """ - all_other_nodes = get_all_discovery_nodes_cached(redis) or [] + all_nodes = get_all_discovery_nodes_cached(redis) or [] visited_node_timestamps_str = redis.get(metrics_visited_nodes) visited_node_timestamps = ( @@ -126,7 +127,10 @@ def consolidate_metrics_from_other_nodes(self, db, redis): merge_app_metrics(new_personal_app_metrics, end_time, db) # Merge & persist metrics for other nodes - for node in all_other_nodes: + for node in all_nodes: + # Skip self + if node["delegateOwnerWallet"] == shared_config["delegate"]["owner_wallet"]: + continue start_time_str = ( visited_node_timestamps[node["endpoint"]] if node["endpoint"] in visited_node_timestamps @@ -237,8 +241,8 @@ def synchronize_all_node_metrics(self, db, redis): monthly_route_metrics = {} daily_app_metrics = {} monthly_app_metrics = {} - all_other_nodes = get_all_discovery_nodes_cached(redis) or [] - for node in all_other_nodes: + all_nodes = get_all_discovery_nodes_cached(redis) or [] + for node in all_nodes: historical_metrics = get_historical_metrics(node["endpoint"]) logger.debug( f"got historical metrics from {node['endpoint']}: {historical_metrics}" diff --git a/packages/discovery-provider/src/tasks/update_clique_signers.py b/packages/discovery-provider/src/tasks/update_clique_signers.py index 1ff28b5a2a8..54a579362cb 100644 --- a/packages/discovery-provider/src/tasks/update_clique_signers.py +++ b/packages/discovery-provider/src/tasks/update_clique_signers.py @@ -3,7 +3,7 @@ import requests from src.tasks.celery_app import celery -from src.utils.get_all_other_nodes import get_all_discovery_nodes_cached +from src.utils.get_all_nodes import get_all_discovery_nodes_cached from src.utils.prometheus_metric import save_duration_metric logger = logging.getLogger(__name__) diff --git a/packages/discovery-provider/src/utils/get_all_other_nodes.py b/packages/discovery-provider/src/utils/get_all_nodes.py similarity index 93% rename from packages/discovery-provider/src/utils/get_all_other_nodes.py rename to packages/discovery-provider/src/utils/get_all_nodes.py index 1e40a3f9fa0..8b839e02247 100644 --- a/packages/discovery-provider/src/utils/get_all_other_nodes.py +++ b/packages/discovery-provider/src/utils/get_all_nodes.py @@ -60,8 +60,8 @@ def get_all_nodes(service_type: bytes) -> Tuple[List[str], List[str]]: ).call() ids_list = list(range(1, num_nodes + 1)) - all_other_nodes = [] - all_other_wallets = [] + all_nodes = [] + all_wallets = [] # fetch all nodes' info in parallel async def fetch_results(): @@ -82,14 +82,14 @@ async def fetch_results(): wallet = node_info[3] endpoint = node_info[1] if is_fqdn(endpoint): - all_other_nodes.append(endpoint) - all_other_wallets.append(wallet) + all_nodes.append(endpoint) + all_wallets.append(wallet) except Exception as e: logger.error( - f"get_all_other_nodes.py | ERROR in fetching node info {node_info} generated {e}" + f"get_all_nodes.py | ERROR in fetching node info {node_info} generated {e}" ) - return all_other_nodes, all_other_wallets + return all_nodes, all_wallets def get_all_discovery_nodes() -> Tuple[List[str], List[str]]: From 0922eb4d2cc65341e8b1c877ace40dd5af4761db Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:09:29 -0800 Subject: [PATCH 62/66] Fix env import --- packages/mobile/src/services/sdk/audius-sdk.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mobile/src/services/sdk/audius-sdk.ts b/packages/mobile/src/services/sdk/audius-sdk.ts index 7d45ce0d12e..776a3aa0cc8 100644 --- a/packages/mobile/src/services/sdk/audius-sdk.ts +++ b/packages/mobile/src/services/sdk/audius-sdk.ts @@ -3,12 +3,11 @@ import { EventEmitter } from 'events' import type { AudiusSdk } from '@audius/sdk' import { AntiAbuseOracleSelector, sdk } from '@audius/sdk' -import { env } from '../env' - import { auth } from './auth' import { discoveryNodeSelectorService } from './discoveryNodeSelector' import { claimableTokensService, rewardManagerService } from './solana' import { getStorageNodeSelector } from './storageNodeSelector' +import { env } from 'app/env' let inProgress = false const SDK_LOADED_EVENT_NAME = 'AUDIUS_SDK_LOADED' From 4e6ef8b955a8386084eaef4774c4bbe37250526c Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:10:43 -0800 Subject: [PATCH 63/66] remove nullcheck --- packages/mobile/src/services/sdk/audius-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mobile/src/services/sdk/audius-sdk.ts b/packages/mobile/src/services/sdk/audius-sdk.ts index 776a3aa0cc8..c957a4cbbf6 100644 --- a/packages/mobile/src/services/sdk/audius-sdk.ts +++ b/packages/mobile/src/services/sdk/audius-sdk.ts @@ -27,7 +27,7 @@ const initSdk = async () => { rewardManagerClient: rewardManagerService, antiAbuseOracleSelector: new AntiAbuseOracleSelector({ endpoints: [env.AAO_ENDPOINT], - registeredAddresses: env.ORACLE_ETH_ADDRESSES?.split(',') ?? [] + registeredAddresses: env.ORACLE_ETH_ADDRESSES.split(',') ?? [] }) } }) From 1acbbceccd1e4ea696748cf4375d4f22dead075b Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:28:37 -0800 Subject: [PATCH 64/66] fix test patch --- .../integration_tests/queries/test_get_attestation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_attestation.py b/packages/discovery-provider/integration_tests/queries/test_get_attestation.py index db95526b5dd..009ad3b27cf 100644 --- a/packages/discovery-provider/integration_tests/queries/test_get_attestation.py +++ b/packages/discovery-provider/integration_tests/queries/test_get_attestation.py @@ -274,7 +274,7 @@ def test_get_attestation_weekly_pool_exhausted(app): @pytest.fixture -def patch_get_all_other_nodes(): +def patch_get_all_nodes(): with patch( "src.queries.get_attestation.get_all_discovery_nodes_cached", return_value=[ @@ -284,7 +284,7 @@ def patch_get_all_other_nodes(): yield -def test_get_create_sender_attestation(app, patch_get_all_other_nodes): +def test_get_create_sender_attestation(app, patch_get_all_nodes): new_sender_address = "0x94e140D27F3d5EE9EcA0109A71CcBa0109964DCa" owner_wallet, sender_attestation = get_create_sender_attestation(new_sender_address) @@ -317,7 +317,7 @@ def test_get_create_sender_attestation(app, patch_get_all_other_nodes): ) -def test_get_create_sender_attestation_not_registered(app, patch_get_all_other_nodes): +def test_get_create_sender_attestation_not_registered(app, patch_get_all_nodes): new_sender_address = "0x04e140D27F3d5EE9EcA0109A71CcBa0109964DCa" with pytest.raises( Exception, From b6ac7987c82bb3e55a4a57b7b6ea7f005fbb870a Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:04:40 -0800 Subject: [PATCH 65/66] forgot to save --- packages/discovery-provider/src/api/v1/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discovery-provider/src/api/v1/helpers.py b/packages/discovery-provider/src/api/v1/helpers.py index fbc3dcb1070..2f9a77f4fc3 100644 --- a/packages/discovery-provider/src/api/v1/helpers.py +++ b/packages/discovery-provider/src/api/v1/helpers.py @@ -21,7 +21,7 @@ ) from src.queries.reactions import ReactionResponse from src.utils.auth_middleware import MESSAGE_HEADER, SIGNATURE_HEADER -from src.utils.get_all_other_nodes import get_all_healthy_content_nodes_cached +from src.utils.get_all_nodes import get_all_healthy_content_nodes_cached from src.utils.helpers import decode_string_id, encode_int_id from src.utils.redis_connection import get_redis from src.utils.rendezvous import RendezvousHash From 604d04422d7ff5204afde28853aec638d71ac181 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:34:27 -0800 Subject: [PATCH 66/66] add missing wallets --- .../tasks/entity_manager/test_index_skip_tx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py index 1d65e3cf41d..d8553ff07df 100644 --- a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py +++ b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_index_skip_tx.py @@ -33,9 +33,9 @@ def get_events_side_effect(_, tx_receipt): mocker.patch( "src.queries.confirm_indexing_transaction_error.get_all_discovery_nodes_cached", return_value=[ - {"endpoint": "node1"}, - {"endpoint": "node2"}, - {"endpoint": "node3"}, + {"endpoint": "node1", "delegateOwnerWallet": "wallet1"}, + {"endpoint": "node2", "delegateOwnerWallet": "wallet2"}, + {"endpoint": "node3", "delegateOwnerWallet": "wallet3"}, ], autospec=True, )