Skip to content

Commit

Permalink
feat(release): update 070b4f3
Browse files Browse the repository at this point in the history
  • Loading branch information
fedellen committed Aug 30, 2024
1 parent 2635b4e commit f3c3cc1
Show file tree
Hide file tree
Showing 29 changed files with 2,984 additions and 3,117 deletions.
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

0 comments on commit f3c3cc1

Please sign in to comment.