-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pro 2758 #145
Pro 2758 #145
Changes from 3 commits
c19a700
2d0d3d0
8ec81b3
1a7179d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,29 @@ import { CronTime } from 'cron'; | |
import { ethers } from "ethers"; | ||
import ErrorMessage from "../constants/ErrorMessage.js"; | ||
import ReturnCode from "../constants/ReturnCode.js"; | ||
import { encode, decode } from "../utils/crypto.js"; | ||
import { encode, decode, verifySignature } from "../utils/crypto.js"; | ||
import SupportedNetworks from "../../config.json" assert { type: "json" }; | ||
import { APIKey } from "../models/api-key.js"; | ||
import { ArkaConfigUpdateData } from "../types/arka-config-dto.js"; | ||
import { ApiKeyDto } from "../types/apikey-dto.js"; | ||
import { CreateSecretCommand, DeleteSecretCommand, GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; | ||
import EtherspotAbi from "../abi/EtherspotAbi.js"; | ||
import { AuthDto } from "../types/auth-dto.js"; | ||
import { IncomingHttpHeaders } from "http"; | ||
|
||
const adminRoutes: FastifyPluginAsync = async (server) => { | ||
|
||
const prefixSecretId = 'arka_'; | ||
|
||
let client: SecretsManagerClient; | ||
|
||
const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; | ||
|
||
if (!unsafeMode) { | ||
client = new SecretsManagerClient(); | ||
} | ||
|
||
|
||
server.post('/adminLogin', async function (request, reply) { | ||
try { | ||
if(!server.config.UNSAFE_MODE) { | ||
|
@@ -76,50 +92,76 @@ const adminRoutes: FastifyPluginAsync = async (server) => { | |
|
||
server.post('/saveKey', async function (request, reply) { | ||
try { | ||
if(!server.config.UNSAFE_MODE) { | ||
return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.NOT_AUTHORIZED }); | ||
} | ||
const body: any = JSON.parse(request.body as string) as ApiKeyDto; | ||
const body = JSON.parse(request.body as string) as ApiKeyDto; | ||
if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); | ||
if (!body.apiKey || !body.privateKey) | ||
if (!body.apiKey) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); | ||
|
||
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); | ||
|
||
const wallet = new ethers.Wallet(body.privateKey); | ||
const mnemonic = ethers.utils.entropyToMnemonic( | ||
ethers.utils.randomBytes(16) | ||
); | ||
const wallet = ethers.Wallet.fromMnemonic(mnemonic); | ||
const privateKey = wallet.privateKey; | ||
const publicAddress = await wallet.getAddress(); | ||
|
||
// Use Sequelize to find the API key | ||
const result = await server.apiKeyRepository.findOneByWalletAddress(publicAddress); | ||
if(!unsafeMode) { | ||
const { 'x-signature': signature, 'x-timestamp': timestamp } = request.headers as IncomingHttpHeaders & AuthDto; | ||
if(!signature || !timestamp) | ||
return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); | ||
if(!verifySignature(signature, request.body as string, timestamp, server.config.HMAC_SECRET)) | ||
return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); | ||
|
||
if (result) { | ||
request.log.error('Duplicate record found'); | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); | ||
} | ||
const command = new GetSecretValueCommand({SecretId: prefixSecretId + body.apiKey}) | ||
const secrets = await client.send(command).catch((err) => err); | ||
|
||
await server.apiKeyRepository.create({ | ||
apiKey: body.apiKey, | ||
walletAddress: publicAddress, | ||
privateKey: encode(body.privateKey, server.config.HMAC_SECRET), | ||
supportedNetworks: body.supportedNetworks, | ||
erc20Paymasters: body.erc20Paymasters, | ||
multiTokenPaymasters: body.multiTokenPaymasters ?? null, | ||
multiTokenOracles: body.multiTokenOracles ?? null, | ||
sponsorName: body.sponsorName ?? null, | ||
logoUrl: body.logoUrl ?? null, | ||
transactionLimit: body.transactionLimit ?? 0, | ||
noOfTransactionsInAMonth: body.noOfTransactionsInAMonth ?? 10, | ||
indexerEndpoint: body.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT, | ||
bundlerApiKey: body.bundlerApiKey ?? null, | ||
}); | ||
if(!(secrets instanceof Error)) { | ||
request.log.error('Duplicate record found'); | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); | ||
} | ||
|
||
const createCommand = new CreateSecretCommand({ | ||
Name: prefixSecretId + body.apiKey, | ||
SecretString: JSON.stringify({ | ||
PRIVATE_KEY: privateKey, | ||
PUBLIC_ADDRESS: publicAddress, | ||
MNEMONIC: mnemonic | ||
}), | ||
}); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to also create a record on the connected db since the sponsorship olivies are handled by that so even in unsafeMode as false create one record on was secrets as above and also include database record creation for the same which also includes addition of policy record by default for the same apiKey There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vignesha22 does this mean we have to delete the record also on /deleteKey endpoint ? |
||
await client.send(createCommand); | ||
} else { | ||
const result = await server.apiKeyRepository.findOneByApiKey(body.apiKey); | ||
if (result) { | ||
request.log.error('Duplicate record found'); | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); | ||
} | ||
|
||
await server.apiKeyRepository.create({ | ||
apiKey: body.apiKey, | ||
walletAddress: publicAddress, | ||
privateKey: encode(privateKey, server.config.HMAC_SECRET), | ||
supportedNetworks: body.supportedNetworks, | ||
erc20Paymasters: body.erc20Paymasters, | ||
multiTokenPaymasters: body.multiTokenPaymasters ?? null, | ||
multiTokenOracles: body.multiTokenOracles ?? null, | ||
sponsorName: body.sponsorName ?? null, | ||
logoUrl: body.logoUrl ?? null, | ||
transactionLimit: body.transactionLimit ?? 0, | ||
noOfTransactionsInAMonth: body.noOfTransactionsInAMonth ?? 10, | ||
indexerEndpoint: body.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT ?? null, | ||
bundlerApiKey: body.bundlerApiKey ?? null, | ||
}); | ||
} | ||
|
||
return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully saved' }); | ||
} catch (err: any) { | ||
request.log.error(err); | ||
return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); | ||
} | ||
}) | ||
}); | ||
|
||
server.post('/updateKey', async function (request, reply) { | ||
try { | ||
|
@@ -173,21 +215,98 @@ const adminRoutes: FastifyPluginAsync = async (server) => { | |
|
||
server.post('/deleteKey', async function (request, reply) { | ||
try { | ||
if(!server.config.UNSAFE_MODE) { | ||
return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.NOT_AUTHORIZED }); | ||
} | ||
const body: any = JSON.parse(request.body as string); | ||
if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); | ||
if (!body) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); | ||
if (!body.apiKey) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); | ||
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); | ||
|
||
const apiKeyInstance = await server.apiKeyRepository.findOneByApiKey(body.apiKey); | ||
if (!apiKeyInstance) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); | ||
let bundlerApiKey = body.apiKey; | ||
let supportedNetworks; | ||
if(!unsafeMode) { | ||
const { 'x-signature': signature, 'x-timestamp': timestamp } = request.headers as IncomingHttpHeaders & AuthDto; | ||
if(!signature || !timestamp || isNaN(+timestamp)) | ||
return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); | ||
if(!verifySignature(signature, request.body as string, timestamp, server.config.HMAC_SECRET)) | ||
return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); | ||
const getSecretCommand = new GetSecretValueCommand({SecretId: prefixSecretId + body.apiKey}); | ||
const secretValue = await client.send(getSecretCommand) | ||
if(secretValue instanceof Error) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); | ||
|
||
const secrets = JSON.parse(secretValue.SecretString ?? '{}'); | ||
|
||
if (!secrets['PRIVATE_KEY']) { | ||
server.log.info("Invalid Api Key provided") | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) | ||
} | ||
if (secrets['BUNDLER_API_KEY']) { | ||
bundlerApiKey = secrets['BUNDLER_API_KEY']; | ||
} | ||
|
||
await server.apiKeyRepository.delete(body.apiKey); | ||
if(secrets['SUPPORTED_NETWORKS']) { | ||
const buffer = Buffer.from(secrets['SUPPORTED_NETWORKS'], 'base64'); | ||
supportedNetworks = JSON.parse(buffer.toString()) | ||
} | ||
supportedNetworks = supportedNetworks ?? SupportedNetworks; | ||
|
||
const privateKey = secrets['PRIVATE_KEY']; | ||
|
||
// native balance check. | ||
const nativeBalancePromiseArr = []; | ||
const nativePaymasterDepositPromiseArr = []; | ||
for(const network of supportedNetworks) { | ||
const provider = new ethers.providers.JsonRpcProvider(network.bundler + '?api-key=' + bundlerApiKey); | ||
const wallet = new ethers.Wallet(privateKey, provider) | ||
nativeBalancePromiseArr.push(wallet.getBalance()); | ||
|
||
const contract = new ethers.Contract( | ||
network.contracts.etherspotPaymasterAddress, | ||
EtherspotAbi, | ||
wallet | ||
); | ||
nativePaymasterDepositPromiseArr.push(contract.getSponsorBalance(wallet.address)); | ||
} | ||
|
||
let error = false; | ||
|
||
await Promise.allSettled([...nativeBalancePromiseArr, ...nativePaymasterDepositPromiseArr]).then((data) => { | ||
const threshold = ethers.utils.parseEther('0.0001'); | ||
for(const item of data) { | ||
if( | ||
item.status === 'fulfilled' && | ||
item.value?.gt(threshold) | ||
) { | ||
error = true; | ||
return; | ||
} | ||
if(item.status === 'rejected') { | ||
request.log.error( | ||
`Error occurred while fetching balance/sponsor balance for apiKey: ${body.apiKey}, reason: ${JSON.stringify(item.reason)}` | ||
); | ||
} | ||
} | ||
}); | ||
|
||
if(error) { | ||
return reply.code(400).send({error: ErrorMessage.BALANCE_EXCEEDS_THRESHOLD }); | ||
} | ||
|
||
const deleteCommand = new DeleteSecretCommand({ | ||
SecretId: prefixSecretId + body.apiKey, | ||
RecoveryWindowInDays: server.config.DELETE_KEY_RECOVER_WINDOW, | ||
}); | ||
|
||
await client.send(deleteCommand); | ||
} else { | ||
const apiKeyInstance = await server.apiKeyRepository.findOneByApiKey(body.apiKey); | ||
if (!apiKeyInstance) | ||
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); | ||
|
||
await server.apiKeyRepository.delete(body.apiKey); | ||
} | ||
|
||
return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully deleted' }); | ||
} catch (err: any) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface AuthDto { | ||
'x-signature': string; | ||
'x-timestamp': string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ch4r10t33r using 7 days because only 7-30 days values are allowed for scheduled deletion.