Filter everything with users (#16)

* Filter everything with users

* Fix backtests and user management

* Add cursor rules

* Fix backtest and bots

* Update configs names

* Sign until unauth

* Setup delegate

* Setup delegate and sign

* refact

* Enhance Privy signature generation with improved cryptographic methods

* Add Fastify backend

* Add Fastify backend routes for privy

* fix privy signing

* fix privy client

* Fix tests

* add gmx core

* fix merging sdk

* Fix tests

* add gmx core

* add gmx core

* add privy to boilerplate

* clean

* fix

* add fastify

* Remove Managing.Fastify submodule

* Add Managing.Fastify as regular directory instead of submodule

* Update .gitignore to exclude Managing.Fastify dist and node_modules directories

* Add token approval functionality to Privy plugin

- Introduced a new endpoint `/approve-token` for approving ERC20 tokens.
- Added `approveToken` method to the Privy plugin for handling token approvals.
- Updated `signPrivyMessage` to differentiate between message signing and token approval requests.
- Enhanced the plugin with additional schemas for input validation.
- Included new utility functions for token data retrieval and message construction.
- Updated tests to verify the new functionality and ensure proper request decoration.

* Add PrivyApproveTokenResponse model for token approval response

- Created a new class `PrivyApproveTokenResponse` to encapsulate the response structure for token approval requests.
- The class includes properties for `Success` status and a transaction `Hash`.

* Refactor trading commands and enhance API routes

- Updated `OpenPositionCommandHandler` to use asynchronous methods for opening trades and canceling orders.
- Introduced new Fastify routes for opening positions and canceling orders with appropriate request validation.
- Modified `EvmManager` to handle both Privy and non-Privy wallet operations, utilizing the Fastify API for Privy wallets.
- Adjusted test configurations to reflect changes in account types and added helper methods for testing Web3 proxy services.

* Enhance GMX trading functionality and update dependencies

- Updated `dev:start` script in `package.json` to include the `-d` flag for Fastify.
- Upgraded `fastify-cli` dependency to version 7.3.0.
- Added `sourceMap` option to `tsconfig.json`.
- Refactored GMX plugin to improve position opening logic, including enhanced error handling and validation.
- Introduced a new method `getMarketInfoFromTicker` for better market data retrieval.
- Updated account type in `PrivateKeys.cs` to use `Privy`.
- Adjusted `EvmManager` to utilize the `direction` enum directly for trade direction handling.

* Refactor GMX plugin for improved trading logic and market data retrieval

- Enhanced the `openGmxPositionImpl` function to utilize the `TradeDirection` enum for trade direction handling.
- Introduced `getTokenDataFromTicker` and `getMarketByIndexToken` functions for better market and token data retrieval.
- Updated collateral calculation and logging for clarity.
- Adjusted `EvmManager` to ensure proper handling of price values in trade requests.

* Refactor GMX plugin and enhance testing for position opening

- Updated `test:single` script in `package.json` to include TypeScript compilation before running tests.
- Removed `this` context from `getClientForAddress` function and replaced logging with `console.error`.
- Improved collateral calculation in `openGmxPositionImpl` for better precision.
- Adjusted type casting for `direction` in the API route to utilize `TradeDirection` enum.
- Added a new test for opening a long position in GMX, ensuring functionality and correctness.

* Update sdk

* Update

* update fastify

* Refactor start script in package.json to simplify command execution

- Removed the build step from the start script, allowing for a more direct launch of the Fastify server.

* Update package.json for Web3Proxy

- Changed the name from "Web3Proxy" to "web3-proxy".
- Updated version from "0.0.0" to "1.0.0".
- Modified the description to "The official Managing Web3 Proxy".

* Update Dockerfile for Web3Proxy

- Upgraded Node.js base image from 18-alpine to 22.14.0-alpine.
- Added NODE_ENV environment variable set to production.

* Refactor Dockerfile and package.json for Web3Proxy

- Removed the build step from the Dockerfile to streamline the image creation process.
- Updated the start script in package.json to include the build step, ensuring the application is built before starting the server.

* Add fastify-tsconfig as a development dependency in Dockerfile-web3proxy

* Remove fastify-tsconfig extension from tsconfig.json for Web3Proxy

* Add PrivyInitAddressResponse model for handling initialization responses

- Introduced a new class `PrivyInitAddressResponse` to encapsulate the response structure for Privy initialization, including properties for success status, USDC hash, order vault hash, and error message.

* Update

* Update

* Remove fastify-tsconfig installation from Dockerfile-web3proxy

* Add build step to Dockerfile-web3proxy

- Included `npm run build` in the Dockerfile to ensure the application is built during the image creation process.

* Update

* approvals

* Open position from front embedded wallet

* Open position from front embedded wallet

* Open position from front embedded wallet

* Fix call contracts

* Fix limit price

* Close position

* Fix close position

* Fix close position

* add pinky

* Refactor position handling logic

* Update Dockerfile-pinky to copy package.json and source code from the correct directory

* Implement password protection modal and enhance UI with new styles; remove unused audio elements and update package dependencies.

* add cancel orders

* Update callContract function to explicitly cast account address as Address type

* Update callContract function to cast transaction parameters as any type for compatibility

* Cast transaction parameters as any type in approveTokenImpl for compatibility

* Cast wallet address and transaction parameters as Address type in approveTokenImpl for type safety

* Add .env configuration file for production setup including database and server settings

* Refactor home route to update welcome message and remove unused SDK configuration code

* add referral code

* fix referral

* Add sltp

* Fix typo

* Fix typo

* setup sltp on backtend

* get orders

* get positions with slp

* fixes

* fixes close position

* fixes

* Remove MongoDB project references from Dockerfiles for managing and worker APIs

* Comment out BotManagerWorker service registration and remove MongoDB project reference from Dockerfile

* fixes
This commit is contained in:
Oda
2025-04-20 22:18:27 +07:00
committed by GitHub
parent 0ae96a3278
commit 528c62a0a1
400 changed files with 94446 additions and 1635 deletions

View File

@@ -0,0 +1,677 @@
import fp from 'fastify-plugin'
import {FastifyReply, FastifyRequest, FastifyInstance} from 'fastify'
import {z} from 'zod'
import canonicalize from 'canonicalize'
import crypto from 'crypto'
import {PrivyClient} from '@privy-io/server-auth'
import {ethers} from 'ethers'
import dotenv from 'dotenv'
// Load environment variables
dotenv.config()
import Token from '../../generated/gmxsdk/abis/Token.json' with { type: 'json' }
import { ARBITRUM } from '../../generated/gmxsdk/configs/chains.js'
import { TOKENS } from '../../generated/gmxsdk/configs/tokens.js'
import { CONTRACTS } from '../../generated/gmxsdk/configs/contracts.js'
import { getClientForAddress, getTokenDataFromTicker } from './gmx.js'
import { Ticker } from '../../generated/ManagingApiTypes.js'
import { Address } from 'viem'
/**
* Privy Plugin
*
* This plugin adds functionality for interacting with the Privy API,
* including signing messages and token approvals.
*
* Token Approval Process:
* 1. Client sends a request to /approve-token with:
* - walletId: The user's wallet ID
* - address: The user's wallet address
* - ticker: The token ticker or enum value (as string)
* - amount: The amount to approve (optional, defaults to max amount)
* - chainId: The chain ID where the approval will take place
*
* 2. The server handles the request by:
* - Validating the input parameters
* - Getting the token data based on the ticker
* - Creating and sending an ERC20 approval transaction using ethers.js and Privy
* - Returning the transaction hash to the client
*/
declare module 'fastify' {
export interface FastifyRequest {
signPrivyMessage: typeof signPrivyMessage;
approveToken: typeof approveToken;
initAddress: typeof initAddress;
}
}
/**
* Returns an initialized PrivyClient instance with configuration from environment variables
* @returns The configured PrivyClient instance
*/
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
const appId = fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID;
const appSecret = fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET;
const authKey = fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY;
if (!appId || !appSecret || !authKey) {
console.error('Missing Privy environment variables:');
console.error('PRIVY_APP_ID:', appId ? 'present' : 'missing');
console.error('PRIVY_APP_SECRET:', appSecret ? 'present' : 'missing');
console.error('PRIVY_AUTHORIZATION_KEY:', authKey ? 'present' : 'missing');
throw new Error('Missing required Privy environment variables');
}
return new PrivyClient(
appId,
appSecret,
{
walletApi: {
authorizationPrivateKey: authKey
}
}
);
};
/**
* Authentication function for Privy API calls
* @param url The URL for the API endpoint
* @param body The request body
* @returns The authorization signature
*/
export function getAuthorizationSignature({url, body}: {url: string; body: object}) {
const payload = {
version: 1,
method: 'POST',
url,
body,
headers: {
'privy-app-id': process.env.PRIVY_APP_ID
}
};
// JSON-canonicalize the payload and convert it to a buffer
// @ts-ignore - canonicalize is a callable function despite TypeScript error
const serializedPayload = canonicalize(payload) as string;
const serializedPayloadBuffer = Buffer.from(serializedPayload);
// Get the authorization key from environment variables
const privateKeyAsString = (process.env.PRIVY_AUTHORIZATION_KEY ?? "").replace('wallet-auth:', '');
// Convert private key to PEM format and create a key object
const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`;
const privateKey = crypto.createPrivateKey({
key: privateKeyAsPem,
format: 'pem',
});
// Sign the payload buffer with the private key
const signatureBuffer = crypto.sign('sha256', serializedPayloadBuffer, privateKey);
const signature = signatureBuffer.toString('base64');
return signature;
}
/**
* Makes a request to the Privy API with proper authentication and headers.
* @param url - The full URL for the API endpoint.
* @param body - The request body.
* @param requiresAuth - Whether the request requires authentication.
* @returns The response data.
*/
export const makePrivyRequest = async <T>(url: string, body: object, requiresAuth = true): Promise<T> => {
try {
let headers: Record<string, string> = {
'Content-Type': 'application/json',
'privy-app-id': process.env.PRIVY_APP_ID ?? "",
};
if (requiresAuth) {
// Generate authorization signature
const authSig = await getAuthorizationSignature({
url,
body
});
// Create authentication string for basic auth
const basicAuthString = `${process.env.PRIVY_APP_ID}:${process.env.PRIVY_APP_SECRET}`;
const base64Auth = Buffer.from(basicAuthString).toString('base64');
headers = {
...headers,
'privy-authorization-signature': authSig,
'Authorization': `Basic ${base64Auth}`
};
}
const request = {
method: 'POST',
headers,
body: JSON.stringify(body)
};
// Make the API request
const response = await fetch(url, request).then(res => {
return res;
}).catch(err => {
console.log("error", err);
throw err;
});
if (!response.ok) {
throw new Error(`Privy API request failed: ${response.status}`);
}
return await response.json() as T;
} catch (error) {
console.error('Error making Privy API request:', error);
throw new Error('Failed to make Privy API request');
}
};
// Schema for sign-message request
const signMessageSchema = z.object({
walletId: z.string().nonempty(),
message: z.string().nonempty(),
address: z.string().nonempty()
});
// Schema for token-approval request
const tokenApprovalSchema = z.object({
walletId: z.string().nonempty(),
address: z.string().nonempty(),
ticker: z.string().nonempty(),
amount: z.bigint().positive().optional(),
chainId: z.number().positive().optional()
});
/**
* Gets the chain name based on chain ID
* @param chainId The chain ID
* @returns The CAIP-2 identifier for the chain
*/
export const getChainName = (chainId: number): string => {
switch (chainId) {
case 1:
return 'eip155:1'; // Ethereum Mainnet
case 42161:
return 'eip155:42161'; // Arbitrum One
case 421613:
return 'eip155:421613'; // Arbitrum Goerli
case 8453:
return 'eip155:8453'; // Base
case 84531:
return 'eip155:84531'; // Base Goerli
default:
return `eip155:${chainId}`;
}
};
/**
* Signs a message using a Privy embedded wallet.
* @param walletId - The ID of the wallet to use for signing.
* @param message - The message to sign.
* @param address - The wallet address to use for signing.
* @returns The signature for the message.
*/
export const signMessage = async (walletId: string, message: string, address: string): Promise<string> => {
try {
const privy = getPrivyClient();
const {signature} = await privy.walletApi.ethereum.signMessage({
address: address,
chainType: 'ethereum',
message: message,
});
return signature;
} catch (error) {
console.error('Error signing message:', error);
throw new Error('Failed to sign message with embedded wallet');
}
};
/**
* Approves a token for spending using Privy wallet (implementation)
* @param walletId The wallet ID to use
* @param walletAddress The wallet address
* @param ticker The token ticker or enum value
* @param amount The amount to approve (optional, defaults to max amount)
* @param chainId The chain ID
* @param spenderAddress The address that will be allowed to spend the tokens
* @returns The transaction hash
*/
export const approveTokenImpl = async (
walletAddress: string,
ticker: string,
chainId?: number,
amount?: bigint,
): Promise<string> => {
try {
// Get token data from ticker
const tokenData = GetToken(ticker);
// Create contract interface for ERC20 token
const contractInterface = new ethers.Interface(Token.abi);
// Max uint256 value for unlimited approval
const approveAmount = amount ?
ethers.parseUnits(amount.toString(), tokenData.decimals) :
ethers.MaxUint256;
// Encode the approve function call
const data = contractInterface.encodeFunctionData("approve", [walletAddress, approveAmount]);
chainId = chainId ?? ARBITRUM;
// Get chain name in CAIP-2 format
const networkName = getChainName(chainId);
const privy = getPrivyClient();
// Send the transaction
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: walletAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: tokenData.address as Address,
data: data,
chainId: chainId,
},
} as any);
return hash;
} catch (error) {
console.error('Error approving token:', error);
throw new Error(`Failed to approve token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
function GetToken(symbol: string) {
return TOKENS[ARBITRUM].find(token => token.symbol === symbol)
}
/**
* Approves a token for spending using Privy wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param address The wallet address
* @param ticker The token ticker or enum value
* @param chainId The chain ID
* @param amount The amount to approve (optional, defaults to max amount)
* @returns The response object with success status and transaction hash
*/
export async function approveToken(
this: FastifyRequest,
reply: FastifyReply,
address: string,
ticker: string,
chainId: number,
amount?: bigint
) {
try {
// Validate the request parameters
tokenApprovalSchema.parse({
address,
ticker,
amount,
chainId
});
if (!address) {
throw new Error('Wallet address is required for token approval');
}
if (!chainId) {
throw new Error('Chain ID is required for token approval');
}
// Call the approveTokenImpl function
const hash = await approveTokenImpl(address, ticker, chainId, amount);
return {
success: true,
hash: hash
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Signs a message using Privy embedded wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param walletId The wallet ID
* @param message The message to sign
* @param address The wallet address
* @returns The signature of the signed message
*/
export async function signPrivyMessage(
this: FastifyRequest,
reply: FastifyReply,
walletId: string,
message: string,
address: string
) {
try {
// Validate the request parameters
signMessageSchema.parse({ walletId, message, address });
// Call the signMessage function
const signature = await signMessage(walletId, message, address);
return {
success: true,
signature: signature
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Approves a specific contract address for spending using Privy wallet
* @param walletAddress The wallet address
* @param tokenAddress The token address to approve
* @param spenderAddress The contract address to approve
* @param chainId The chain ID
* @param amount The amount to approve (optional, defaults to max amount)
* @returns The transaction hash
*/
export const approveContractImpl = async (
walletAddress: string,
tokenAddress: string,
spenderAddress: string,
chainId?: number,
amount?: bigint,
): Promise<string> => {
try {
// Create contract interface for ERC20 token
const contractInterface = new ethers.Interface(Token.abi);
// Max uint256 value for unlimited approval
const approveAmount = amount ?? ethers.MaxUint256;
// Encode the approve function call
const data = contractInterface.encodeFunctionData("approve", [spenderAddress, approveAmount]);
chainId = chainId ?? ARBITRUM;
// Get chain name in CAIP-2 format
const networkName = getChainName(chainId);
const privy = getPrivyClient();
// Send the transaction
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: walletAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: tokenAddress as Address,
data: data,
chainId: chainId,
},
} as any);
return hash;
} catch (error) {
console.error('Error approving contract:', error);
throw new Error(`Failed to approve contract: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Gets the current allowance for a token for the GMX OrderVault contract
* @param ownerAddress The address of the token owner
* @param tokenAddress The address of the token contract
* @returns The current allowance as a BigNumber
*/
export const getTokenAllowance = async (
ownerAddress: string,
tokenAddress: string,
spenderAddress: string
): Promise<bigint> => {
try {
// use gmx sdk to get allowance
const sdk = await getClientForAddress(ownerAddress);
const allowance = await sdk.executeMulticall({
token: {
contractAddress: tokenAddress,
abiId: "ERC20",
calls: {
allowance: {
methodName: "allowance",
params: [ownerAddress, spenderAddress]
}
}
}
}).
then(res => {
return res.data.token.allowance.returnValues[0];
});
return allowance;
} catch (error) {
console.error('Error getting token allowance:', error);
throw new Error(`Failed to get token allowance: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Initializes a wallet address by approving USDC for GMX trading and GMX OrderVault
* @param address The wallet address to initialize
* @returns The transaction hashes for both approvals
*/
export const initAddressImpl = async (
address: string,
): Promise<{ usdcHash: string, orderVaultHash: string, exchangeRouterHash: string }> => {
try {
const sdk = await getClientForAddress(address);
const {tokensData} = await sdk.tokens.getTokensData();
const usdcTokenData = getTokenDataFromTicker(Ticker.USDC, tokensData);
const wrapperEtherData = getTokenDataFromTicker("WETH", tokensData);
let approveAmount = usdcTokenData.prices.maxPrice; // Large enough amount for trading
// Check approval for USDC
const usdcToken = GetToken('USDC');
const usdcAllowance = await getTokenAllowance(address, usdcToken.address, address);
let usdcHash = "";
if (usdcAllowance < approveAmount) {
// First approve USDC token for GMX trading
const usdcToken = GetToken('USDC');
usdcHash = await approveTokenImpl(
address,
usdcToken.symbol,
ARBITRUM,
usdcTokenData.prices.maxPrice
);
}else{
usdcHash = "Already allowed :" + usdcAllowance;
}
const orderVaultAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].OrderVault);
// Then approve GMX OrderVault with the correct amount
let orderVaultHash = "";
if (orderVaultAllowance < approveAmount) {
orderVaultHash = await approveContractImpl(
address,
usdcToken.address,
CONTRACTS[ARBITRUM].OrderVault,
ARBITRUM,
approveAmount
);
}else{
orderVaultHash = "Already allowed :" + orderVaultAllowance;
}
const wrapperEtherAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].OrderVault);
let wrapperEtherHash = "";
if (wrapperEtherAllowance < approveAmount) {
wrapperEtherHash = await approveContractImpl(
address,
wrapperEtherData.address,
CONTRACTS[ARBITRUM].OrderVault,
ARBITRUM,
approveAmount
);
}else{
wrapperEtherHash = "Already allowed :" + wrapperEtherAllowance;
}
console.log('wrapperEtherAllowance', wrapperEtherAllowance)
const exchangeRouterAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].ExchangeRouter);
console.log('exchangeRouterAllowance', exchangeRouterAllowance)
let exchangeRouterHash = "";
if (exchangeRouterAllowance < approveAmount) {
exchangeRouterHash = await approveContractImpl(
address,
usdcToken.address,
CONTRACTS[ARBITRUM].ExchangeRouter,
ARBITRUM,
approveAmount
);
}else{
exchangeRouterHash = "Already allowed :" + exchangeRouterAllowance;
}
const wrapperEtherExchangeAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].ExchangeRouter);
let wrapperEtherExchangeHash = "";
if (wrapperEtherExchangeAllowance < approveAmount) {
wrapperEtherExchangeHash = await approveContractImpl(
address,
wrapperEtherData.address,
CONTRACTS[ARBITRUM].ExchangeRouter,
ARBITRUM,
approveAmount
);
}else{
wrapperEtherExchangeHash = "Already allowed :" + wrapperEtherExchangeAllowance;
}
console.log('wrapperEtherExchangeAllowance', wrapperEtherExchangeAllowance)
const usdcSyntheticRouterAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].SyntheticsRouter);
let usdcSyntheticRouterHash = "";
if (usdcSyntheticRouterAllowance < approveAmount) {
usdcSyntheticRouterHash = await approveContractImpl(
address,
usdcToken.address,
CONTRACTS[ARBITRUM].SyntheticsRouter,
ARBITRUM,
approveAmount
);
}else{
usdcSyntheticRouterHash = "Already allowed :" + usdcSyntheticRouterAllowance;
}
console.log('usdcSyntheticRouterAllowance', usdcSyntheticRouterAllowance)
console.log('usdcHash', usdcHash)
console.log('orderVaultHash', orderVaultHash)
console.log('exchangeRouterHash', exchangeRouterHash)
return {
usdcHash,
orderVaultHash,
exchangeRouterHash
};
} catch (error) {
console.error('Error initializing address:', error);
throw new Error(`Failed to initialize address: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Initializes a wallet address for GMX trading
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param address The wallet address to initialize
* @returns The response object with success status and transaction hashes
*/
export async function initAddress(
this: FastifyRequest,
reply: FastifyReply,
address: string
) {
try {
if (!address) {
throw new Error('Wallet address is required for initialization');
}
const { usdcHash, orderVaultHash } = await initAddressImpl(address);
return {
success: true,
usdcHash,
orderVaultHash
};
} catch (error) {
this.log.error(error);
reply.status(500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* The use of fastify-plugin is required to be able
* to export the decorators to the outer scope
*
* @see {@link https://github.com/fastify/fastify-plugin}
*/
export default fp(async (fastify) => {
// Decorate request with methods that use the Fastify instance
fastify.decorateRequest('signPrivyMessage', async function(this: FastifyRequest, reply: FastifyReply, walletId: string, message: string, address: string) {
return signPrivyMessage.call(this, reply, walletId, message, address);
});
fastify.decorateRequest('approveToken', async function(this: FastifyRequest, reply: FastifyReply, address: string, ticker: string, chainId: number, amount?: bigint) {
return approveToken.call(this, reply, address, ticker, chainId, amount);
});
fastify.decorateRequest('initAddress', async function(this: FastifyRequest, reply: FastifyReply, address: string) {
return initAddress.call(this, reply, address);
});
// Test the Privy client initialization
try {
const testClient = getPrivyClient(fastify);
fastify.log.info('Privy client initialized successfully');
} catch (error) {
fastify.log.error('Failed to initialize Privy client:', error);
throw error;
}
}, {
name: 'privy-plugin'
});