-
Notifications
You must be signed in to change notification settings - Fork 2
✨ server: add allower #839
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
base: main
Are you sure you want to change the base?
Changes from all commits
512e598
efdd6db
9e0fadd
0712b36
5f7769a
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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@exactly/server": patch | ||
| --- | ||
|
|
||
| ✨ use gcp kms for allower |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@exactly/server": patch | ||
| --- | ||
|
|
||
| ✨ poke account after kyc |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,6 +175,7 @@ | |
| "valibot", | ||
| "valierror", | ||
| "valkey", | ||
| "valora", | ||
| "viem", | ||
| "viewability", | ||
| "wagmi", | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,25 +15,21 @@ import createDebug from "debug"; | |
| import { eq, inArray } from "drizzle-orm"; | ||
| import { Hono } from "hono"; | ||
| import * as v from "valibot"; | ||
| import { bytesToBigInt, hexToBigInt, withRetry } from "viem"; | ||
| import { bytesToBigInt, hexToBigInt } from "viem"; | ||
|
|
||
| import { | ||
| auditorAbi, | ||
| exaAccountFactoryAbi, | ||
| exaPluginAbi, | ||
| exaPreviewerAbi, | ||
| exaPreviewerAddress, | ||
| marketAbi, | ||
| upgradeableModularAccountAbi, | ||
| wethAddress, | ||
| } from "@exactly/common/generated/chain"; | ||
| import { Address, Hash, Hex } from "@exactly/common/validation"; | ||
|
|
||
| import database, { cards, credentials } from "../database"; | ||
| import { keeper } from "../utils/accounts"; | ||
| import { createWebhook, findWebhook, headerValidator, network } from "../utils/alchemy"; | ||
| import appOrigin from "../utils/appOrigin"; | ||
| import decodePublicKey from "../utils/decodePublicKey"; | ||
| import keeper from "../utils/keeper"; | ||
| import { sendPushNotification } from "../utils/onesignal"; | ||
| import { autoCredit } from "../utils/panda"; | ||
| import publicClient from "../utils/publicClient"; | ||
|
|
@@ -96,7 +92,7 @@ export default new Hono().post( | |
| category !== "erc1155" && | ||
| (rawContract?.rawValue && rawContract.rawValue !== "0x" ? hexToBigInt(rawContract.rawValue) > 0n : !!value), | ||
| ); | ||
| const accounts = await database.query.credentials | ||
| const accountLookup = await database.query.credentials | ||
| .findMany({ | ||
| columns: { account: true, publicKey: true, factory: true, source: true }, | ||
| where: inArray(credentials.account, [...new Set(transfers.map(({ toAddress }) => toAddress))]), | ||
|
|
@@ -109,18 +105,16 @@ export default new Hono().post( | |
| ), | ||
| ), | ||
| ); | ||
| if (Object.keys(accounts).length === 1) setUser({ id: v.parse(Address, Object.keys(accounts)[0]) }); | ||
| if (Object.keys(accountLookup).length === 1) setUser({ id: v.parse(Address, Object.keys(accountLookup)[0]) }); | ||
|
|
||
| const marketsByAsset = await publicClient | ||
| .readContract({ address: exaPreviewerAddress, functionName: "assets", abi: exaPreviewerAbi }) | ||
| .then((p) => new Map<Address, Address>(p.map((m) => [v.parse(Address, m.asset), v.parse(Address, m.market)]))); | ||
| const markets = new Set(marketsByAsset.values()); | ||
| const pokes = new Map< | ||
| Address, | ||
| { assets: Set<Address>; factory: Address; publicKey: Uint8Array<ArrayBuffer>; source: null | string } | ||
| >(); | ||
|
|
||
| const accounts = new Set<Address>(); | ||
| for (const { toAddress: account, rawContract, value, asset: assetSymbol } of transfers) { | ||
| if (!accounts[account]) continue; | ||
| if (!accountLookup[account]) continue; | ||
| if (rawContract?.address && markets.has(rawContract.address)) continue; | ||
| const asset = rawContract?.address ?? ETH; | ||
| const underlying = asset === ETH ? WETH : asset; | ||
|
|
@@ -131,141 +125,84 @@ export default new Hono().post( | |
| en: `${value ? `${value} ` : ""}${assetSymbol} received${marketsByAsset.has(underlying) ? " and instantly started earning yield" : ""}`, | ||
| }, | ||
| }).catch((error: unknown) => captureException(error)); | ||
|
|
||
| if (pokes.has(account)) { | ||
| pokes.get(account)?.assets.add(asset); | ||
| } else { | ||
| const { publicKey, factory, source } = accounts[account]; | ||
| pokes.set(account, { publicKey, factory, source, assets: new Set([asset]) }); | ||
| } | ||
| accounts.add(account); | ||
| } | ||
| const { "sentry-trace": sentryTrace, baggage } = getTraceData(); | ||
| Promise.allSettled( | ||
| [...pokes].map(([account, { publicKey, factory, source, assets }]) => | ||
| continueTrace({ sentryTrace, baggage }, () => | ||
| withScope((scope) => | ||
| startSpan( | ||
| { name: "account activity", op: "exa.activity", attributes: { account }, forceTransaction: true }, | ||
| async (span) => { | ||
| scope.setUser({ id: account }); | ||
| const isDeployed = !!(await publicClient.getCode({ address: account })); | ||
| scope.setTag("exa.new", !isDeployed); | ||
| if (!isDeployed) { | ||
| try { | ||
| await keeper.exaSend( | ||
| { name: "create account", op: "exa.account", attributes: { account } }, | ||
| { | ||
| address: factory, | ||
| functionName: "createAccount", | ||
| args: [0n, [decodePublicKey(publicKey, bytesToBigInt)]], | ||
| abi: exaAccountFactoryAbi, | ||
| }, | ||
| ); | ||
| track({ event: "AccountFunded", userId: account, properties: { source } }); | ||
| } catch (error: unknown) { | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: "account_failed" }); | ||
| throw error; | ||
| } | ||
| } | ||
| if (assets.has(ETH)) assets.delete(WETH); | ||
| const results = await Promise.allSettled( | ||
| [...assets] | ||
| .filter((asset) => marketsByAsset.has(asset) || asset === ETH) | ||
| .map(async (asset) => | ||
| withRetry( | ||
| () => | ||
| keeper | ||
| .exaSend( | ||
| { name: "poke account", op: "exa.poke", attributes: { account, asset } }, | ||
| { | ||
| address: account, | ||
| abi: [...exaPluginAbi, ...upgradeableModularAccountAbi, ...auditorAbi, ...marketAbi], | ||
| ...(asset === ETH | ||
| ? { functionName: "pokeETH" } | ||
| : { | ||
| functionName: "poke", | ||
| args: [marketsByAsset.get(asset)!], // eslint-disable-line @typescript-eslint/no-non-null-assertion | ||
| }), | ||
| }, | ||
| { ignore: ["NoBalance()"] }, | ||
| ) | ||
| .then((receipt) => { | ||
| if (receipt) return receipt; | ||
| throw new Error("NoBalance()"); | ||
| }), | ||
| [...accounts] | ||
| .flatMap((account) => { | ||
| const info = accountLookup[account]; | ||
| return info ? [[account, info] as const] : []; | ||
| }) | ||
| .map(([account, { publicKey, factory, source }]) => | ||
| continueTrace({ sentryTrace, baggage }, () => | ||
| withScope((scope) => | ||
| startSpan( | ||
| { name: "account activity", op: "exa.activity", attributes: { account }, forceTransaction: true }, | ||
| async (span) => { | ||
| scope.setUser({ id: account }); | ||
| scope.setTag("exa.account", account); | ||
| const isDeployed = !!(await publicClient.getCode({ address: account })); | ||
| scope.setTag("exa.new", !isDeployed); | ||
| if (!isDeployed) { | ||
| try { | ||
| await keeper.exaSend( | ||
| { name: "create account", op: "exa.account", attributes: { account } }, | ||
| { | ||
| delay: 2000, | ||
| retryCount: 5, | ||
| shouldRetry: ({ error }) => { | ||
| if (error instanceof Error && error.message === "NoBalance()") return true; | ||
| withScope((captureScope) => { | ||
| captureScope.setUser({ id: account }); | ||
| captureException(error, { level: "error", fingerprint: revertFingerprint(error) }); | ||
| }); | ||
| return true; | ||
| }, | ||
| address: factory, | ||
| functionName: "createAccount", | ||
| args: [0n, [decodePublicKey(publicKey, bytesToBigInt)]], | ||
| abi: exaAccountFactoryAbi, | ||
| }, | ||
| ), | ||
| ), | ||
| ); | ||
| for (const result of results) { | ||
| if (result.status === "fulfilled") continue; | ||
| if (result.reason instanceof Error && result.reason.message === "NoBalance()") { | ||
| withScope((captureScope) => { | ||
| captureScope.setUser({ id: account }); | ||
| captureScope.addEventProcessor((event) => { | ||
| if (event.exception?.values?.[0]) event.exception.values[0].type = "NoBalance"; | ||
| return event; | ||
| }); | ||
| captureException(result.reason, { | ||
| level: "warning", | ||
| fingerprint: ["{{ default }}", "NoBalance"], | ||
| }); | ||
| }); | ||
| continue; | ||
| ); | ||
| track({ event: "AccountFunded", userId: account, properties: { source } }); | ||
| } catch (error: unknown) { | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: "account_failed" }); | ||
| throw error; | ||
| } | ||
| } | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: "poke_failed" }); | ||
| throw result.reason; | ||
| } | ||
| autoCredit(account) | ||
| .then(async (auto) => { | ||
| span.setAttribute("exa.autoCredit", auto); | ||
| if (!auto) return; | ||
| const credential = await database.query.credentials.findFirst({ | ||
| where: eq(credentials.account, account), | ||
| columns: {}, | ||
| with: { | ||
| cards: { | ||
| columns: { id: true, mode: true }, | ||
| where: inArray(cards.status, ["ACTIVE", "FROZEN"]), | ||
| await keeper | ||
| .poke(account, { ignore: [`NotAllowed(${account})`] }) | ||
aguxez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .catch((error: unknown) => captureException(error, { level: "error" })); | ||
|
Comment on lines
+164
to
+166
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. 🚩 NoBalance() special handling removed without replacement The old The new code passes The corresponding test ( Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| autoCredit(account) | ||
| .then(async (auto) => { | ||
| span.setAttribute("exa.autoCredit", auto); | ||
| if (!auto) return; | ||
| const credential = await database.query.credentials.findFirst({ | ||
| where: eq(credentials.account, account), | ||
| columns: {}, | ||
| with: { | ||
| cards: { | ||
| columns: { id: true, mode: true }, | ||
| where: inArray(cards.status, ["ACTIVE", "FROZEN"]), | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| if (!credential || credential.cards.length === 0) return; | ||
| const card = credential.cards[0]; | ||
| span.setAttribute("exa.card", card?.id); | ||
| if (card?.mode !== 0) return; | ||
| await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id)); | ||
| span.setAttribute("exa.mode", 1); | ||
| sendPushNotification({ | ||
| userId: account, | ||
| headings: { en: "Card mode changed" }, | ||
| contents: { en: "Credit mode activated" }, | ||
| }).catch((error: unknown) => captureException(error)); | ||
| }) | ||
| .catch((error: unknown) => captureException(error)); | ||
| span.setStatus({ code: SPAN_STATUS_OK }); | ||
| }, | ||
| }); | ||
| if (!credential || credential.cards.length === 0) return; | ||
| const card = credential.cards[0]; | ||
| span.setAttribute("exa.card", card?.id); | ||
| if (card?.mode !== 0) return; | ||
| await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id)); | ||
| span.setAttribute("exa.mode", 1); | ||
| sendPushNotification({ | ||
| userId: account, | ||
| headings: { en: "Card mode changed" }, | ||
| contents: { en: "Credit mode activated" }, | ||
| }).catch((error: unknown) => captureException(error, { level: "error" })); | ||
| }) | ||
| .catch((error: unknown) => captureException(error, { level: "error" })); | ||
| span.setStatus({ code: SPAN_STATUS_OK }); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ).catch((error: unknown) => { | ||
| withScope((scope) => { | ||
| scope.setUser({ id: account }); | ||
| captureException(error, { level: "error", fingerprint: revertFingerprint(error) }); | ||
| }); | ||
| throw error; | ||
| }), | ||
| ), | ||
| ).catch((error: unknown) => { | ||
| withScope((captureScope) => { | ||
| captureScope.setUser({ id: account }); | ||
| captureException(error, { level: "error", fingerprint: revertFingerprint(error) }); | ||
| }); | ||
| throw error; | ||
| }), | ||
| ), | ||
| ) | ||
| .then((results) => { | ||
| getActiveSpan()?.setStatus( | ||
|
|
@@ -274,7 +211,7 @@ export default new Hono().post( | |
| : { code: SPAN_STATUS_ERROR, message: "activity_failed" }, | ||
| ); | ||
| }) | ||
| .catch((error: unknown) => captureException(error)); | ||
| .catch((error: unknown) => captureException(error, { level: "error" })); | ||
| return c.json({}); | ||
| }, | ||
| ); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.