Skip to content
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

feat(release): update 070b4f3 #6

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use-strict";

process.env.CRYPTO_FUND_EXCLUDED_ADDRESSES = "testExcludedAddress";

// Mocha configuration file
// Reference for options: https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.js
module.exports = {
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"indep",
"Irys",
"knexfile",
"Kyve",
"lamports",
"livemode",
"nvmrc",
Expand All @@ -26,8 +27,11 @@
"sats",
"solana",
"sslmode",
"tendermint",
"tkyve",
"trivago",
"typecheck",
"ukyve",
"uncategorized",
"winc",
"winstons"
Expand Down
29 changes: 19 additions & 10 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,52 +47,52 @@ export const electronicallySuppliedServicesTaxCode = "txcd_10000000"; //cspell:d
export const paymentAmountLimits: CurrencyLimitations = {
aud: {
minimumPaymentAmount: 7500,
maximumPaymentAmount: 15_000_00,
maximumPaymentAmount: 3_000_00,
suggestedPaymentAmounts: [25_00, 75_00, 150_00],
},
brl: {
minimumPaymentAmount: 2500,
maximumPaymentAmount: 50_000_00,
maximumPaymentAmount: 10_000_00,
suggestedPaymentAmounts: [125_00, 250_00, 500_00],
},
cad: {
minimumPaymentAmount: 500,
maximumPaymentAmount: 15_000_00,
maximumPaymentAmount: 2_000_00,
suggestedPaymentAmounts: [25_00, 50_00, 100_00],
},
eur: {
minimumPaymentAmount: 500,
maximumPaymentAmount: 10_000_00,
maximumPaymentAmount: 2_000_00,
suggestedPaymentAmounts: [25_00, 50_00, 100_00],
},
gbp: {
minimumPaymentAmount: 500,
maximumPaymentAmount: 10_000_00,
maximumPaymentAmount: 2_000_00,
suggestedPaymentAmounts: [20_00, 40_00, 80_00],
},
hkd: {
minimumPaymentAmount: 5000,
maximumPaymentAmount: 100_000_00,
maximumPaymentAmount: 20_000_00,
suggestedPaymentAmounts: [200_00, 400_00, 800_00],
},
inr: {
minimumPaymentAmount: 50_000,
maximumPaymentAmount: 900_000_00,
maximumPaymentAmount: 180_000_00,
suggestedPaymentAmounts: [2000_00, 4000_00, 8000_00],
},
jpy: {
minimumPaymentAmount: 750,
maximumPaymentAmount: 1_500_000,
maximumPaymentAmount: 300_000,
suggestedPaymentAmounts: [3_500, 6_500, 15_000],
},
sgd: {
minimumPaymentAmount: 750,
maximumPaymentAmount: 15_000_00,
maximumPaymentAmount: 3_000_00,
suggestedPaymentAmounts: [25_00, 75_00, 150_00],
},
usd: {
minimumPaymentAmount: 500,
maximumPaymentAmount: 10_000_00,
maximumPaymentAmount: 2_000_00,
suggestedPaymentAmounts: [25_00, 50_00, 100_00],
},
};
Expand Down Expand Up @@ -335,7 +335,16 @@ export const solanaGatewayUrl = new URL(
process.env.SOLANA_GATEWAY || "https://api.mainnet-beta.solana.com/"
);

export const kyveGatewayUrl = new URL(
process.env.KYVE_GATEWAY || "https://api.kyve.network/"
);

const thirtyMinutesMs = 1000 * 60 * 30;
export const topUpQuoteExpirationMs = +(
process.env.TOP_UP_QUOTE_EXPIRATION_MS ?? thirtyMinutesMs
);

export const cryptoFundExcludedAddresses = process.env
.CRYPTO_FUND_EXCLUDED_ADDRESSES
? process.env.CRYPTO_FUND_EXCLUDED_ADDRESSES.split(",")
: [];
162 changes: 160 additions & 2 deletions src/consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Message } from "@aws-sdk/client-sqs";
import { Message, SQSClient } from "@aws-sdk/client-sqs";
import { Consumer, ConsumerOptions } from "sqs-consumer";

import { Architecture } from "./architecture";
import { DestinationAddressType } from "./database/dbTypes";
import { PostgresDatabase } from "./database/postgres";
import { MandrillEmailProvider } from "./emailProvider";
import { addCreditsToAddresses } from "./jobs/addCreditsToAddresses";
import { creditPendingTransactionsHandler } from "./jobs/creditPendingTx";
import globalLogger from "./logger";
import { MetricRegistry } from "./metricRegistry";
import { isValidUserAddress } from "./utils/base64";
import { loadSecretsToEnv } from "./utils/loadSecretsToEnv";
import { sendSlackMessage } from "./utils/slack";

export function createConsumerQueue(
function createConsumerQueue(
{ queueUrl, ...restOfOptions }: ConsumerOptions,
metricOnError: () => void = () =>
MetricRegistry.uncaughtExceptionCounter.inc(),
Expand Down Expand Up @@ -73,3 +82,152 @@ export function createConsumerQueue(

return consumer;
}

function startPendingPaymentTxQueue({
paymentDatabase,
}: Partial<Architecture>): void {
const pendingPaymentTxQueueUrl = process.env.PENDING_PAYMENT_TX_QUEUE_URL;
if (!pendingPaymentTxQueueUrl) {
globalLogger.warn(`No pending payment tx queue URL found!`);
return;
}

const paymentTxQueueLogger = globalLogger.child({
queue: "pending-payment-tx",
});

return createConsumerQueue(
{
sqs: new SQSClient({ region: process.env.AWS_REGION ?? "us-east-1" }),
queueUrl: pendingPaymentTxQueueUrl,
// Queue is cron based, so we can afford to wait a bit on polling
pollingWaitTimeMs: 10_000, // 10 seconds

handleMessage: async (message: Message) => {
await creditPendingTransactionsHandler({
logger: paymentTxQueueLogger.child({
messageId: message.MessageId,
}),
paymentDatabase,
});
return;
},
},
() => MetricRegistry.creditPendingTxJobFailure.inc(),
paymentTxQueueLogger
).start();
}

type AdminCreditToolMessageBody = {
addresses: string[];
creditAmount: number;
addressType?: DestinationAddressType;
giftMessage?: string;
};

class AdminCreditToolInputError extends Error {
constructor(message: string) {
super(message);
this.name = "AdminCreditToolInputError";
}
}

function startAdminCreditToolConsumer({
emailProvider,
paymentDatabase,
}: Partial<Architecture>): void {
const adminCreditToolQueueUrl = process.env.ADMIN_CREDIT_TOOL_QUEUE_URL;
if (!adminCreditToolQueueUrl) {
globalLogger.warn(`No admin credit tool queue URL found!`);
return;
}

const adminCreditToolLogger = globalLogger.child({
queue: "admin-credit-tool",
});

return createConsumerQueue(
{
sqs: new SQSClient({ region: process.env.AWS_REGION ?? "us-east-1" }),
queueUrl: adminCreditToolQueueUrl,
pollingWaitTimeMs: 5_000, // 5 seconds

handleMessage: async (message: Message) => {
try {
if (!message.Body) {
throw new AdminCreditToolInputError(
`No message body found in SQS message to run job on`
);
}

const {
addresses,
creditAmount,
addressType = "arweave",
giftMessage,
} = JSON.parse(message.Body) as AdminCreditToolMessageBody;

if (!addresses || !creditAmount || !addresses.length) {
throw new AdminCreditToolInputError(
`Missing required fields in message body: \`addresses\` and \`creditAmount\``
);
}

if (addressType !== "email") {
for (const address of addresses) {
if (!isValidUserAddress(address, addressType)) {
throw new AdminCreditToolInputError(
`Invalid address for ${addressType} address type: ${address}`
);
}
}
}

await addCreditsToAddresses({
paymentDatabase,
emailProvider,
logger: adminCreditToolLogger.child({
messageId: message.MessageId,
}),
addresses,
addressType,
creditAmount,
giftMessage,
});
} catch (error) {
await sendSlackMessage({
message: `Error processing admin credit tool message:\n${
error instanceof Error ? error.message : error
}`,
icon_emoji: ":x:",
});

if (error instanceof AdminCreditToolInputError) {
adminCreditToolLogger.error(
`Error processing admin credit tool message: ${error.message}`
);
// Don't rethrow input errors, delete this message from the queue
return;
}
throw error;
}
},
},
() => MetricRegistry.adminCreditToolJobFailure.inc(),
adminCreditToolLogger
).start();
}

export async function startConsumers(): Promise<void> {
await loadSecretsToEnv();

const consumerArchitecture: Partial<Architecture> = {
paymentDatabase: new PostgresDatabase({}),
emailProvider: process.env.MANDRILL_API_KEY
? new MandrillEmailProvider(process.env.MANDRILL_API_KEY)
: undefined,
};

startPendingPaymentTxQueue(consumerArchitecture);
startAdminCreditToolConsumer(consumerArchitecture);
}
7 changes: 6 additions & 1 deletion src/database/dbTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ export interface PaymentAdjustment extends Adjustment {

export type UserAddress = string | PublicArweaveAddress;

export const userAddressTypes = ["arweave", "solana", "ethereum"] as const;
export const userAddressTypes = [
"arweave",
"solana",
"ethereum",
"kyve",
] as const;
export type UserAddressType = (typeof userAddressTypes)[number];

export const destinationAddressTypes = [...userAddressTypes, "email"] as const;
Expand Down
20 changes: 19 additions & 1 deletion src/database/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,32 @@ export class PaymentTransactionNotFound extends Error {
}

export class PaymentTransactionHasWrongTarget extends Error {
constructor(transactionId: string, targetAddress: string) {
constructor(transactionId: string, targetAddress?: string) {
super(
`Payment transaction '${transactionId}' has wrong target address '${targetAddress}'`
);
this.name = "PaymentTransactionHasWrongTarget";
}
}

export class TransactionNotAPaymentTransaction extends Error {
constructor(transactionId: string) {
super(
`Transaction with id '${transactionId}' is not a payment transaction!`
);
this.name = "TransactionNotAPaymentTransaction";
}
}

export class PaymentTransactionRecipientOnExcludedList extends Error {
constructor(transactionId: string, senderAddress: string) {
super(
`Payment transaction '${transactionId}' has sender that is on the excluded address list: '${senderAddress}'`
);
this.name = "PaymentTransactionRecipientOnExcludedList";
}
}

export class BadRequest extends Error {
constructor(message: string) {
super(message);
Expand Down
Loading