From f437d506a267ce60ef6ad2050d3915c4e1e0cdae Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 28 Aug 2024 17:57:55 -0700 Subject: [PATCH 01/15] Create transfer tempora workflow --- apps/workers/src/client.ts | 86 ++++++++++++------- apps/workers/src/worker.ts | 28 ++++-- package.json | 1 + packages/workflows/src/all-activities.ts | 3 +- packages/workflows/src/all-workflows.ts | 3 +- .../src/distribution-workflow/activities.ts | 2 +- .../src/distribution-workflow/workflow.ts | 4 +- .../src/transfer-workflow/activities.ts | 82 ++++++++++++++++++ .../src/transfer-workflow/supabase.ts | 10 +++ .../workflows/src/transfer-workflow/wagmi.ts | 58 +++++++++++++ .../src/transfer-workflow/workflow.ts | 47 ++++++++++ 11 files changed, 279 insertions(+), 45 deletions(-) create mode 100644 packages/workflows/src/transfer-workflow/activities.ts create mode 100644 packages/workflows/src/transfer-workflow/supabase.ts create mode 100644 packages/workflows/src/transfer-workflow/wagmi.ts create mode 100644 packages/workflows/src/transfer-workflow/workflow.ts diff --git a/apps/workers/src/client.ts b/apps/workers/src/client.ts index fdec13f87..f3eb01622 100644 --- a/apps/workers/src/client.ts +++ b/apps/workers/src/client.ts @@ -1,43 +1,71 @@ import { Connection, Client } from '@temporalio/client' -import { - // WorkflowA, WorkflowB, +import { TransferWorkflow } from '@my/workflows/workflows' +import type { UserOperation } from 'permissionless' - DistributionsWorkflow, -} from '@my/workflows/all-workflows' +// async function runDistributionWorkflow() { +// const connection = await Connection.connect() // Connect to localhost with default ConnectionOptions. +// // In production, pass options to the Connection constructor to configure TLS and other settings. +// // This is optional but we leave this here to remind you there is a gRPC connection being established. -export async function runWorkflow(): Promise { - const connection = await Connection.connect() // Connect to localhost with default ConnectionOptions. - // In production, pass options to the Connection constructor to configure TLS and other settings. - // This is optional but we leave this here to remind you there is a gRPC connection being established. +// const client = new Client({ +// connection, +// // In production you will likely specify `namespace` here; it is 'default' if omitted +// }) +// // Invoke the `DistributionWorkflow` Workflow, only resolved when the workflow completes +// const handle = await client.workflow.start(DistributionsWorkflow, { +// taskQueue: 'dev', +// workflowId: 'distributions-workflow', // TODO: remember to replace this with a meaningful business ID +// args: [], // type inference works! args: [name: string] +// }) +// console.log('Started handle', handle.workflowId) +// // optional: wait for client result +// const result = await handle.result() + +// return result +// } + +async function runTransferWorkflow(userOp: UserOperation<'v0.7'>) { + const connection = await Connection.connect() const client = new Client({ connection, - // In production you will likely specify `namespace` here; it is 'default' if omitted }) - // Invoke the `DistributionWorkflow` Workflow, only resolved when the workflow completes - const handle = await client.workflow.start(DistributionsWorkflow, { - taskQueue: 'dev', - workflowId: 'distributions-workflow', // TODO: remember to replace this with a meaningful business ID - args: [], // type inference works! args: [name: string] + const handle = await client.workflow.start(TransferWorkflow, { + taskQueue: 'monorepo', + workflowId: `transfers-workflow-${userOp.sender}-${userOp.nonce.toString()}`, // TODO: remember to replace this with a meaningful business ID + args: [userOp], }) - console.log('Started handle', handle) - // // Invoke the `WorkflowA` Workflow, only resolved when the workflow completes - // const result = await client.workflow.execute(WorkflowA, { - // taskQueue: 'monorepo', - // workflowId: `workflow-a-${Date.now()}`, // TODO: remember to replace this with a meaningful business ID - // args: ['Temporal'], // type inference works! args: [name: string] - // }) - // // Starts the `WorkflowB` Workflow, don't wait for it to complete - // await client.workflow.start(WorkflowB, { - // taskQueue: 'monorepo', - // workflowId: `workflow-b-${Date.now()}`, // TODO: remember to replace this with a meaningful business ID - // }) - // console.log(result) // // [api-server] A: Hello, Temporal!, B: Hello, Temporal! - // return result + console.log('Started handle', handle.workflowId) + // optional: wait for client result + const result = await handle.result() + console.log('result: ', result) + + return result } -runWorkflow().catch((err) => { +// runDistributionWorkflow().catch((err) => { +// console.error(err) +// process.exit(1) +// }) + +runTransferWorkflow({ + callData: + '0x34fcd5be000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000713ddc85a615beaec95333736d80c406732f6d7600000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000', + callGasLimit: 100000n, + maxFeePerGas: 1000000110n, + maxPriorityFeePerGas: 1000000000n, + nonce: 1n, + paymaster: '0x592e1224D203Be4214B15e205F6081FbbaCFcD2D', + paymasterData: '0x', + paymasterPostOpGasLimit: 100000n, + paymasterVerificationGasLimit: 150000n, + preVerificationGas: 70000n, + sender: '0x713ddC85a615BEaec95333736D80C406732f6d76', + signature: + '0x01000066ce986500000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000017000000000000000000000000000000000000000000000000000000000000000193e778a488b82629b608dabe2a0979742f065662e670ca4b3e365162bff5457e6fd8931f1d72ab0ba388a92725cf7dba903799639c4cffb45bc232ef9dcb1da2000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008f7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22415141415a7336595a66796251683649754d4d6a4e774f35657171626f3930573058594f644b714d33345742314e4c35484c4250222c226f726967696e223a22687474703a2f2f6c6f63616c686f73743a33303030222c2263726f73734f726967696e223a66616c73657d0000000000000000000000000000000000', + verificationGasLimit: 550000n, +}).catch((err) => { console.error(err) process.exit(1) }) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index a8abc47ce..e9daa07f5 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -1,5 +1,8 @@ import { Worker } from '@temporalio/worker' -import { createActivities } from '@my/workflows/all-activities' +import { + createTransferActivities, + createDistributionActivities, +} from '@my/workflows/all-activities' import { URL, fileURLToPath } from 'node:url' import path from 'node:path' @@ -8,20 +11,27 @@ async function run() { `../../../packages/workflows/src/all-workflows${path.extname(import.meta.url)}`, import.meta.url ) - // Step 1: Register Workflows and Activities with the Worker and connect to // the Temporal server. - const worker = await Worker.create({ + const transferWorker = await Worker.create({ workflowsPath: fileURLToPath(workflowsPathUrl), - activities: createActivities( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.SUPABASE_SERVICE_ROLE - ), - taskQueue: 'dev', + activities: { + ...createTransferActivities( + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE + ), + // ...createDistributionActivities( + // process.env.NEXT_PUBLIC_SUPABASE_URL, + // process.env.SUPABASE_SERVICE_ROLE + // ), + }, + namespace: 'default', + taskQueue: 'monorepo', bundlerOptions: { ignoreModules: ['@supabase/supabase-js'], }, }) + // Worker connects to localhost by default and uses console.error for logging. // Customize the Worker by passing more options to create(): // https://typescript.temporal.io/api/classes/worker.Worker @@ -30,7 +40,7 @@ async function run() { // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls // Step 2: Start accepting tasks on the `monorepo` queue - await worker.run() + await transferWorker.run() // You may create multiple Workers in a single process in order to poll on multiple task queues. } diff --git a/package.json b/package.json index 3ae346903..a8bc7bc9b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "playwright": "yarn workspace @my/playwright", "distributor": "yarn workspace distributor", "snaplet": "yarn workspace @my/snaplet", + "workers": "yarn workspace workers", "shovel": "yarn workspace @my/shovel", "clean": "yarn workspaces foreach --all -pi run clean" }, diff --git a/packages/workflows/src/all-activities.ts b/packages/workflows/src/all-activities.ts index d11c74d46..efba65ba6 100644 --- a/packages/workflows/src/all-activities.ts +++ b/packages/workflows/src/all-activities.ts @@ -1,3 +1,2 @@ -// export * from './workflowA/activities/activitiesA' -// export * from './workflowA/activities/activitiesB' +export * from './transfer-workflow/activities' export * from './distribution-workflow/activities' diff --git a/packages/workflows/src/all-workflows.ts b/packages/workflows/src/all-workflows.ts index 381c6989a..2f3b1486c 100644 --- a/packages/workflows/src/all-workflows.ts +++ b/packages/workflows/src/all-workflows.ts @@ -1,3 +1,2 @@ -// export * from './workflowA/workflow' -// export * from './workflowB/workflow' +export * from './transfer-workflow/workflow' export * from './distribution-workflow/workflow' diff --git a/packages/workflows/src/distribution-workflow/activities.ts b/packages/workflows/src/distribution-workflow/activities.ts index 5b984cb61..1db8f8a34 100644 --- a/packages/workflows/src/distribution-workflow/activities.ts +++ b/packages/workflows/src/distribution-workflow/activities.ts @@ -19,7 +19,7 @@ const inBatches = (array: T[], batchSize = Math.max(8, cpuCount - 1)) => { ) } -export function createActivities(supabaseUrl: string, supabaseKey: string) { +export function createDistributionActivities(supabaseUrl: string, supabaseKey: string) { globalThis.process = globalThis.process || {} globalThis.process.env.SUPABASE_URL = supabaseUrl // HACK: set the supabase url in the environment globalThis.process.env.SUPABASE_SERVICE_ROLE = supabaseKey // HACK: set the supabase key in the environment diff --git a/packages/workflows/src/distribution-workflow/workflow.ts b/packages/workflows/src/distribution-workflow/workflow.ts index 64f028d3a..17cf82ddb 100644 --- a/packages/workflows/src/distribution-workflow/workflow.ts +++ b/packages/workflows/src/distribution-workflow/workflow.ts @@ -1,12 +1,12 @@ // workflows.ts import { proxyActivities, log, ApplicationFailure } from '@temporalio/workflow' -import type { createActivities } from './activities' +import type { createDistributionActivities } from './activities' const { calculateDistributionSharesActivity, fetchDistributionActivity, fetchAllOpenDistributionsActivity, -} = proxyActivities>({ +} = proxyActivities>({ startToCloseTimeout: '30 seconds', }) diff --git a/packages/workflows/src/transfer-workflow/activities.ts b/packages/workflows/src/transfer-workflow/activities.ts new file mode 100644 index 000000000..cd662fd78 --- /dev/null +++ b/packages/workflows/src/transfer-workflow/activities.ts @@ -0,0 +1,82 @@ +import { log, ApplicationFailure } from '@temporalio/activity' +import { fetchTransfer } from './supabase' +import { + simulateUserOperation, + sendUserOperation, + waitForTransactionReceipt, + generateTransferUserOp, +} from './wagmi' +import type { TransferWorkflowArgs } from './workflow' +import { isAddress, type Hex } from 'viem' +export function createTransferActivities(supabaseUrl: string, supabaseKey: string) { + globalThis.process = globalThis.process || {} + globalThis.process.env.SUPABASE_URL = supabaseUrl // HACK: set the supabase url in the environment + globalThis.process.env.SUPABASE_SERVICE_ROLE = supabaseKey // HACK: set the supabase key in the environment + + return { + sendUserOpActivity, + fetchTransferActivity, + waitForTransactionReceiptActivity, + } +} + +async function sendUserOpActivity(args: TransferWorkflowArgs) { + const { sender, to, token, amount, nonce } = args + const parsedAmount = BigInt(amount) + const parsedNonce = BigInt(nonce) + if (!!sender && !isAddress(sender)) + throw ApplicationFailure.nonRetryable('Invalid send account address') + if (!!to && !isAddress(to)) throw ApplicationFailure.nonRetryable('Invalid to address') + if (!token || !isAddress(token)) throw ApplicationFailure.nonRetryable('Invalid token address') + if (typeof parsedAmount !== 'bigint' || parsedAmount <= 0n) + throw ApplicationFailure.nonRetryable('Invalid amount') + if (typeof parsedNonce !== 'bigint' || parsedNonce < 0n) + throw ApplicationFailure.nonRetryable('Invalid nonce') + try { + const userOp = await generateTransferUserOp({ + sender, + to, + token, + amount: parsedAmount, + nonce: parsedNonce, + }) + userOp.signature = args.signature + console.log('userOp: ', userOp) + + const hash = await sendUserOperation(userOp) + console.log('hash: ', hash) + log.info('sendUserOperationActivity', { hash, userOp }) + return hash + } catch (error) { + throw ApplicationFailure.nonRetryable('Error sending user operation', error.code, error) + } +} + +async function waitForTransactionReceiptActivity(hash: `0x${string}`) { + try { + const receipt = await waitForTransactionReceipt(hash) + if (!receipt.success) + throw ApplicationFailure.nonRetryable('Tx failed', receipt.sender, receipt.userOpHash) + log.info('waitForTransactionReceiptActivity', { receipt }) + return receipt + } catch (error) { + throw ApplicationFailure.nonRetryable('Error waiting for tx receipt', error.code, error) + } +} + +async function fetchTransferActivity(hash: `0x${string}`) { + const { data: transfer, error } = await fetchTransfer(hash) + if (error) { + if (error.code === 'PGRST116') { + log.info('fetchTransferActivity', { error }) + return null + } + throw ApplicationFailure.nonRetryable( + 'Error fetching transfer from activity column.', + error.code, + error + ) + } + log.info('fetchTransferActivity', { transfer }) + return transfer +} diff --git a/packages/workflows/src/transfer-workflow/supabase.ts b/packages/workflows/src/transfer-workflow/supabase.ts new file mode 100644 index 000000000..8f5bb2c6a --- /dev/null +++ b/packages/workflows/src/transfer-workflow/supabase.ts @@ -0,0 +1,10 @@ +import { hexToBytea } from 'app/utils/hexToBytea' +import { supabaseAdmin } from 'app/utils/supabase/admin' + +export async function fetchTransfer(hash: `0x${string}`) { + return await supabaseAdmin + .from('send_account_transfers') + .select('*', { count: 'exact', head: true }) + .eq('tx_hash', hexToBytea(hash)) + .single() +} diff --git a/packages/workflows/src/transfer-workflow/wagmi.ts b/packages/workflows/src/transfer-workflow/wagmi.ts new file mode 100644 index 000000000..c009fde4b --- /dev/null +++ b/packages/workflows/src/transfer-workflow/wagmi.ts @@ -0,0 +1,58 @@ +import { log, ApplicationFailure } from '@temporalio/activity' +import type { UserOperation } from 'permissionless' +import { + baseMainnetBundlerClient, + baseMainnetClient, + sendAccountAbi, + tokenPaymasterAddress, + entryPointAddress, +} from '@my/wagmi' +import { encodeFunctionData, erc20Abi, isAddress, type Hex } from 'viem' + +/** + * default user op with preset gas values that work will probably need to move this to the database. + * Paymaster post-op gas limit could be set dynamically based on the status of the paymaster if the price cache is + * outdated, otherwise, a lower post op gas limit around only 50K is needed. In case of needing to update cached price, + * the post op uses around 75K gas. + * + * - [example no update price](https://www.tdly.co/shared/simulation/a0122fae-a88c-47cd-901c-02de87901b45) + * - [Failed due to OOG](https://www.tdly.co/shared/simulation/c259922c-8248-4b43-b340-6ebbfc69bcea) + */ +export const defaultUserOp: Pick< + UserOperation<'v0.7'>, + | 'callGasLimit' + | 'verificationGasLimit' + | 'preVerificationGas' + | 'maxFeePerGas' + | 'maxPriorityFeePerGas' + | 'paymasterVerificationGasLimit' + | 'paymasterPostOpGasLimit' +> = { + callGasLimit: 100000n, + verificationGasLimit: 550000n, + preVerificationGas: 70000n, + maxFeePerGas: 10000000n, + maxPriorityFeePerGas: 10000000n, + paymasterVerificationGasLimit: 150000n, + paymasterPostOpGasLimit: 100000n, +} + +export async function simulateUserOperation(userOp: UserOperation<'v0.7'>) { + return await baseMainnetClient.call({ + account: entryPointAddress[baseMainnetClient.chain.id], + to: userOp.sender, + data: userOp.callData, + }) +} + +export async function sendUserOperation(userOp: UserOperation<'v0.7'>) { + const hash = await baseMainnetBundlerClient.sendUserOperation({ + userOperation: userOp, + }) + return hash +} + +export async function waitForTransactionReceipt(hash: `0x${string}`) { + const receipt = await baseMainnetBundlerClient.waitForUserOperationReceipt({ hash }) + return receipt +} diff --git a/packages/workflows/src/transfer-workflow/workflow.ts b/packages/workflows/src/transfer-workflow/workflow.ts new file mode 100644 index 000000000..caab455e8 --- /dev/null +++ b/packages/workflows/src/transfer-workflow/workflow.ts @@ -0,0 +1,47 @@ +import { proxyActivities, ApplicationFailure, defineQuery, setHandler } from '@temporalio/workflow' +import type { createTransferActivities } from './activities' +import type { Hex } from 'viem' + +const { sendUserOpActivity, waitForTransactionReceiptActivity, fetchTransferActivity } = + proxyActivities>({ + startToCloseTimeout: '30 seconds', + }) + +type simulating = { status: 'simulating'; data: { userOp: UserOperation<'v0.7'> } } +type sending = { status: 'sending'; data: { userOp: UserOperation<'v0.7'> } } +type waiting = { status: 'waiting'; data: { hash: string; userOp: UserOperation<'v0.7'> } } +type indexing = { + status: 'indexing' + data: { receipt: GetUserOperationReceiptReturnType; userOp: UserOperation<'v0.7'> } +} +type confirmed = { + status: 'confirmed' + data: { receipt: GetUserOperationReceiptReturnType; userOp: UserOperation<'v0.7'> } +} + +export type transferState = simulating | sending | waiting | indexing | confirmed + +export const getTransferStateQuery = defineQuery('getTransferState') + +export type TransferWorkflowArgs = { + sender: Hex + to: Hex + token?: Hex + amount: string + nonce: string + signature: Hex +} + +export async function TransferWorkflow(args: TransferWorkflowArgs) { + const hash = await sendUserOpActivity(args) + if (!hash) throw ApplicationFailure.nonRetryable('No hash returned from sendUserOperation') + setHandler(getTransferStateQuery, () => ({ status: 'waiting', data: { userOp, hash } })) + const receipt = await waitForTransactionReceiptActivity(hash) + if (!receipt) + throw ApplicationFailure.nonRetryable('No receipt returned from waitForTransactionReceipt') + setHandler(getTransferStateQuery, () => ({ status: 'indexing', data: { userOp, receipt } })) + const transfer = await fetchTransferActivity(receipt.userOpHash) + if (!transfer) throw ApplicationFailure.retryable('Transfer not yet indexed in db') + setHandler(getTransferStateQuery, () => ({ status: 'confirmed', data: { userOp, receipt } })) + return transfer +} From d05f078b86cb60b8648b505ebdb80530265c90ee Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 28 Aug 2024 17:59:18 -0700 Subject: [PATCH 02/15] New trpc route that starts transfer workflow --- packages/api/package.json | 2 + packages/api/src/routers/_app.ts | 2 + packages/api/src/routers/transfer.ts | 104 +++++++++++++++++++++++++++ packages/temporal/package.json | 38 ++++++++++ tilt/apps.Tiltfile | 1 + tilt/deps.Tiltfile | 11 +++ yarn.lock | 2 + 7 files changed, 160 insertions(+) create mode 100644 packages/api/src/routers/transfer.ts create mode 100644 packages/temporal/package.json diff --git a/packages/api/package.json b/packages/api/package.json index 863e8406c..60de2ccd2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -5,7 +5,9 @@ "private": true, "dependencies": { "@my/supabase": "workspace:*", + "@my/temporal": "workspace:*", "@my/wagmi": "workspace:*", + "@my/workflows": "workspace:*", "@supabase/supabase-js": "2.44.2", "@tanstack/react-query": "^5.18.1", "@trpc/client": "11.0.0-next-beta.264", diff --git a/packages/api/src/routers/_app.ts b/packages/api/src/routers/_app.ts index b7f8b326f..311fe3629 100644 --- a/packages/api/src/routers/_app.ts +++ b/packages/api/src/routers/_app.ts @@ -6,6 +6,7 @@ import { distributionRouter } from './distribution' import { tagRouter } from './tag' import { secretShopRouter } from './secretShop' import { sendAccountRouter } from './sendAccount' +import { transferRouter } from './transfer' import { accountRecoveryRouter } from './account-recovery/router' import { referralsRouter } from './referrals' @@ -18,6 +19,7 @@ export const appRouter = createTRPCRouter({ secretShop: secretShopRouter, sendAccount: sendAccountRouter, referrals: referralsRouter, + transfer: transferRouter, }) export type AppRouter = typeof appRouter diff --git a/packages/api/src/routers/transfer.ts b/packages/api/src/routers/transfer.ts new file mode 100644 index 000000000..c616a2df2 --- /dev/null +++ b/packages/api/src/routers/transfer.ts @@ -0,0 +1,104 @@ +import { TRPCError } from '@trpc/server' +import debug from 'debug' +import { z } from 'zod' +import { createTRPCRouter, protectedProcedure } from '../trpc' +import { client } from '@my/temporal/client' +import type { UserOperation } from 'permissionless' +import { TransferWorkflow, type transferState } from '@my/workflows' +import type { coinsDict } from 'app/data/coins' + +const log = debug('api:transfer') + +export const transferRouter = createTRPCRouter({ + withUserOp: protectedProcedure + .input( + z.object({ + userOp: z.custom>(), + token: z.custom(), //@ todo: might be safer to decode the token from the userOp, to ensure we don't apply the wrong token + }) + ) + .mutation(async ({ input: { token, userOp } }) => { + const { sender, nonce } = userOp + try { + const handle = await client.workflow.start(TransferWorkflow, { + taskQueue: 'monorepo', + workflowId: `transfer-workflow-${token}-${sender}-${nonce}`, + args: [userOp], + }) + log('Started transfer handle', handle.workflowId) + // optional: wait for client result + return await handle.workflowId + } catch (error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error instanceof Error ? error.message : 'Unknown error', + }) + } + }), + getState: protectedProcedure.input(z.string()).query(async ({ input: workflowId }) => { + try { + const handle = await client.workflow.getHandle(workflowId) + const state = await handle.query('getTransferState') + return state + } catch (error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error instanceof Error ? error.message : 'Unknown error', + }) + } + }), + getPending: protectedProcedure + .input( + z.object({ + token: z.custom(), + sender: z.string(), + }) + ) + .query(async ({ input: { token, sender } }) => { + try { + const states: transferState[] = [] + const workflows = await client.workflow.list({ + query: `ExecutionStatus = "Running" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`, + }) + for await (const workflow of workflows) { + const handle = await client.workflow.getHandle(workflow.workflowId) + + const state = await handle.query('getTransferState') + states.push(state) + } + return states + } catch (error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error instanceof Error ? error.message : 'Unknown error', + }) + } + }), + getFailed: protectedProcedure + .input( + z.object({ + token: z.custom(), + sender: z.string(), + }) + ) + .query(async ({ input: { token, sender } }) => { + try { + const states: transferState[] = [] + const workflows = await client.workflow.list({ + query: `ExecutionStatus = "Failed" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`, + }) + for await (const workflow of workflows) { + const handle = await client.workflow.getHandle(workflow.workflowId) + const state = await handle.query('getTransferState') + states.push(state) + } + return states + } catch (error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error instanceof Error ? error.message : 'Unknown error', + cause: error, + }) + } + }), +}) diff --git a/packages/temporal/package.json b/packages/temporal/package.json new file mode 100644 index 000000000..d47c437c3 --- /dev/null +++ b/packages/temporal/package.json @@ -0,0 +1,38 @@ +{ + "version": "0.0.0", + "name": "@my/temporal", + "type": "module", + "files": [ + "package.json", + "src" + ], + "exports": { + "./payload-converter": { + "types": "./src/payload-converter.ts", + "require": "./build/payload-converter.cjs", + "default": "./src/payload-converter.ts" + }, + "./client": { + "types": "./src/client.ts", + "default": "./src/client.ts" + } + }, + "scripts": { + "lint": "tsc", + "server": "temporal server start-dev --db-filename ./var/temporal.db", + "build": "esbuild --bundle --outfile=build/payload-converter.cjs --target=esnext --platform=node --external:@temporalio/common --external:@bufbuild/protobuf src/payload-converter.ts" + }, + "dependencies": { + "@temporalio/client": "^1.10.1", + "@temporalio/common": "^1.11.1", + "superjson": "^2.2.1" + }, + "peerDependencies": { + "typescript": "^5.5.3" + }, + "devDependencies": { + "esbuild": "^0.23.1", + "temporal": "^0.7.1", + "typescript": "^5.5.3" + } +} diff --git a/tilt/apps.Tiltfile b/tilt/apps.Tiltfile index 054aae685..6ef49b0c5 100644 --- a/tilt/apps.Tiltfile +++ b/tilt/apps.Tiltfile @@ -115,6 +115,7 @@ local_resource( "supabase", "supabase:generate", "wagmi:generate", + "temporal:build", "temporal", ], serve_cmd = "yarn workspace workers start", diff --git a/tilt/deps.Tiltfile b/tilt/deps.Tiltfile index cb48f6ccb..42d24a05f 100644 --- a/tilt/deps.Tiltfile +++ b/tilt/deps.Tiltfile @@ -247,6 +247,17 @@ local_resource( ), ) +local_resource( + name = "temporal:build", + allow_parallel = True, + cmd = "yarn workspace @my/temporal build", + labels = labels, + resource_deps = [ + "yarn:install", + ], + deps = ui_files, +) + local_resource( name = "shovel:generate-config", allow_parallel = True, diff --git a/yarn.lock b/yarn.lock index 95012fb6f..8fd671e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6213,7 +6213,9 @@ __metadata: resolution: "@my/api@workspace:packages/api" dependencies: "@my/supabase": "workspace:*" + "@my/temporal": "workspace:*" "@my/wagmi": "workspace:*" + "@my/workflows": "workspace:*" "@supabase/supabase-js": "npm:2.44.2" "@tanstack/react-query": "npm:^5.18.1" "@trpc/client": "npm:11.0.0-next-beta.264" From ea4dfeb299dd48de4dc3bd1969416b0ac6cffbf8 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Mon, 2 Sep 2024 16:17:18 -0700 Subject: [PATCH 03/15] Encapsulate temporal into a package --- apps/workers/src/worker.ts | 6 +--- .../src/transfer-workflow/supabase.ts | 18 ++++++++-- .../src/transfer-workflow/workflow.ts | 33 +++++++++---------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index e9daa07f5..71ec4354b 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -7,10 +7,6 @@ import { URL, fileURLToPath } from 'node:url' import path from 'node:path' async function run() { - const workflowsPathUrl = new URL( - `../../../packages/workflows/src/all-workflows${path.extname(import.meta.url)}`, - import.meta.url - ) // Step 1: Register Workflows and Activities with the Worker and connect to // the Temporal server. const transferWorker = await Worker.create({ @@ -40,7 +36,7 @@ async function run() { // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls // Step 2: Start accepting tasks on the `monorepo` queue - await transferWorker.run() + await worker.run() // You may create multiple Workers in a single process in order to poll on multiple task queues. } diff --git a/packages/workflows/src/transfer-workflow/supabase.ts b/packages/workflows/src/transfer-workflow/supabase.ts index 8f5bb2c6a..1760f1219 100644 --- a/packages/workflows/src/transfer-workflow/supabase.ts +++ b/packages/workflows/src/transfer-workflow/supabase.ts @@ -1,10 +1,24 @@ +import { log, ApplicationFailure } from '@temporalio/activity' import { hexToBytea } from 'app/utils/hexToBytea' import { supabaseAdmin } from 'app/utils/supabase/admin' -export async function fetchTransfer(hash: `0x${string}`) { - return await supabaseAdmin +export async function isTransferIndexed(hash: `0x${string}`) { + const { data, error } = await supabaseAdmin .from('send_account_transfers') .select('*', { count: 'exact', head: true }) .eq('tx_hash', hexToBytea(hash)) .single() + + if (error) { + if (error.code === 'PGRST116') { + log.info('isTransferIndexedActivity', { error }) + return null + } + throw ApplicationFailure.nonRetryable( + 'Error reading transfer from send_account_transfers column.', + error.code, + error + ) + } + return data !== null } diff --git a/packages/workflows/src/transfer-workflow/workflow.ts b/packages/workflows/src/transfer-workflow/workflow.ts index caab455e8..340a210d6 100644 --- a/packages/workflows/src/transfer-workflow/workflow.ts +++ b/packages/workflows/src/transfer-workflow/workflow.ts @@ -1,11 +1,16 @@ import { proxyActivities, ApplicationFailure, defineQuery, setHandler } from '@temporalio/workflow' import type { createTransferActivities } from './activities' -import type { Hex } from 'viem' +import type { UserOperation, GetUserOperationReceiptReturnType } from 'permissionless' -const { sendUserOpActivity, waitForTransactionReceiptActivity, fetchTransferActivity } = - proxyActivities>({ - startToCloseTimeout: '30 seconds', - }) +const { + simulateUserOpActivity, + sendUserOpActivity, + waitForTransactionReceiptActivity, + isTransferIndexedActivity, +} = proxyActivities>({ + // TODO: make this configurable + startToCloseTimeout: '45 seconds', +}) type simulating = { status: 'simulating'; data: { userOp: UserOperation<'v0.7'> } } type sending = { status: 'sending'; data: { userOp: UserOperation<'v0.7'> } } @@ -23,24 +28,18 @@ export type transferState = simulating | sending | waiting | indexing | confirme export const getTransferStateQuery = defineQuery('getTransferState') -export type TransferWorkflowArgs = { - sender: Hex - to: Hex - token?: Hex - amount: string - nonce: string - signature: Hex -} - -export async function TransferWorkflow(args: TransferWorkflowArgs) { - const hash = await sendUserOpActivity(args) +export async function TransferWorkflow(userOp: UserOperation<'v0.7'>) { + setHandler(getTransferStateQuery, () => ({ status: 'simulating', data: { userOp } })) + await simulateUserOpActivity(userOp) + setHandler(getTransferStateQuery, () => ({ status: 'sending', data: { userOp } })) + const hash = await sendUserOpActivity(userOp) if (!hash) throw ApplicationFailure.nonRetryable('No hash returned from sendUserOperation') setHandler(getTransferStateQuery, () => ({ status: 'waiting', data: { userOp, hash } })) const receipt = await waitForTransactionReceiptActivity(hash) if (!receipt) throw ApplicationFailure.nonRetryable('No receipt returned from waitForTransactionReceipt') setHandler(getTransferStateQuery, () => ({ status: 'indexing', data: { userOp, receipt } })) - const transfer = await fetchTransferActivity(receipt.userOpHash) + const transfer = await isTransferIndexedActivity(receipt.userOpHash) if (!transfer) throw ApplicationFailure.retryable('Transfer not yet indexed in db') setHandler(getTransferStateQuery, () => ({ status: 'confirmed', data: { userOp, receipt } })) return transfer From ce99b32a92d1caf576a34dfcf2f7f0d36a6e5bc1 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 28 Aug 2024 18:00:15 -0700 Subject: [PATCH 04/15] Call transfer trpc mutation from frontend --- packages/api/src/routers/transfer.ts | 8 +- packages/app/features/send/confirm/screen.tsx | 153 ++++++------------ packages/app/features/send/screen.test.tsx | 10 ++ .../app/utils/useUserOpTransferMutation.ts | 1 + 4 files changed, 62 insertions(+), 110 deletions(-) diff --git a/packages/api/src/routers/transfer.ts b/packages/api/src/routers/transfer.ts index c616a2df2..5c7ece373 100644 --- a/packages/api/src/routers/transfer.ts +++ b/packages/api/src/routers/transfer.ts @@ -5,7 +5,7 @@ import { createTRPCRouter, protectedProcedure } from '../trpc' import { client } from '@my/temporal/client' import type { UserOperation } from 'permissionless' import { TransferWorkflow, type transferState } from '@my/workflows' -import type { coinsDict } from 'app/data/coins' +import type { allCoins } from 'app/data/coins' const log = debug('api:transfer') @@ -14,7 +14,7 @@ export const transferRouter = createTRPCRouter({ .input( z.object({ userOp: z.custom>(), - token: z.custom(), //@ todo: might be safer to decode the token from the userOp, to ensure we don't apply the wrong token + token: z.custom(), //@ todo: might be safer to decode the token from the userOp, to ensure we don't apply the wrong token }) ) .mutation(async ({ input: { token, userOp } }) => { @@ -50,7 +50,7 @@ export const transferRouter = createTRPCRouter({ getPending: protectedProcedure .input( z.object({ - token: z.custom(), + token: z.custom(), sender: z.string(), }) ) @@ -77,7 +77,7 @@ export const transferRouter = createTRPCRouter({ getFailed: protectedProcedure .input( z.object({ - token: z.custom(), + token: z.custom(), sender: z.string(), }) ) diff --git a/packages/app/features/send/confirm/screen.tsx b/packages/app/features/send/confirm/screen.tsx index 626562f88..77ef7f225 100644 --- a/packages/app/features/send/confirm/screen.tsx +++ b/packages/app/features/send/confirm/screen.tsx @@ -2,7 +2,6 @@ import { Avatar, Button, ButtonText, - isWeb, Label, LinkableAvatar, Paragraph, @@ -14,41 +13,32 @@ import { YStack, type YStackProps, } from '@my/ui' -import { baseMainnet } from '@my/wagmi' +import { baseMainnet, baseMainnetClient, entryPointAddress } from '@my/wagmi' import { useQueryClient } from '@tanstack/react-query' import { IconAccount } from 'app/components/icons' -import { useTokenActivityFeed } from 'app/features/home/utils/useTokenActivityFeed' +import { IconCoin } from 'app/components/icons/IconCoin' import { useSendScreenParams } from 'app/routers/params' import { assert } from 'app/utils/assert' import formatAmount, { localizeAmount } from 'app/utils/formatAmount' -import { hexToBytea } from 'app/utils/hexToBytea' import { useSendAccount } from 'app/utils/send-accounts' import { shorten } from 'app/utils/strings' import { throwIf } from 'app/utils/throwIf' import { useProfileLookup } from 'app/utils/useProfileLookup' import { useUSDCFees } from 'app/utils/useUSDCFees' -import { - useGenerateTransferUserOp, - useUserOpTransferMutation, -} from 'app/utils/useUserOpTransferMutation' +import { useGenerateTransferUserOp } from 'app/utils/useUserOpTransferMutation' import { useAccountNonce } from 'app/utils/userop' -import { - type Activity, - isSendAccountReceiveEvent, - isSendAccountTransfersEvent, -} from 'app/utils/zod/activity' import { useEffect, useState } from 'react' import { useRouter } from 'solito/router' -import { formatUnits, type Hex, isAddress } from 'viem' +import { formatUnits, isAddress } from 'viem' import { useEstimateFeesPerGas } from 'wagmi' import { useCoin } from 'app/provider/coins' import { useCoinFromSendTokenParam } from 'app/utils/useCoinFromTokenParam' import { allCoinsDict } from 'app/data/coins' -import { IconCoin } from 'app/components/icons/IconCoin' - -import debug from 'debug' - -const log = debug('app:features:send:confirm:screen') +import { api } from 'app/utils/api' +import { TRPCClientError } from '@trpc/client' +import { getUserOperationHash } from 'permissionless' +import { signUserOp } from 'app/utils/signUserOp' +import { byteaToBase64 } from 'app/utils/byteaToBase64' export function SendConfirmScreen() { const [queryParams] = useSendScreenParams() @@ -78,6 +68,11 @@ export function SendConfirmScreen() { export function SendConfirm() { const [queryParams] = useSendScreenParams() const { sendToken, recipient, idType, amount } = queryParams + const { + mutateAsync: transfer, + isPending: isTransferPending, + isError: isTransferError, + } = api.transfer.withUserOp.useMutation() const queryClient = useQueryClient() const { data: sendAccount, isLoading: isSendAccountLoading } = useSendAccount() @@ -96,7 +91,6 @@ export function SendConfirm() { sendAccount?.send_account_credentials .filter((c) => !!c.webauthn_credentials) .map((c) => c.webauthn_credentials as NonNullable) ?? [] - const [sentTxHash, setSentTxHash] = useState() const router = useRouter() @@ -131,21 +125,9 @@ export function SendConfirm() { } = useEstimateFeesPerGas({ chainId: baseMainnet.id, }) - const { - mutateAsync: sendUserOp, - isPending: isTransferPending, - isError: isTransferError, - submittedAt, - } = useUserOpTransferMutation() const [error, setError] = useState() - const { data: transfers, error: tokenActivityError } = useTokenActivityFeed({ - address: sendToken === 'eth' ? undefined : hexToBytea(sendToken), - refetchInterval: sentTxHash ? 1000 : undefined, // refetch every second if we have sent a tx - enabled: !!sentTxHash, - }) - const hasEnoughBalance = selectedCoin?.balance && selectedCoin.balance >= BigInt(amount ?? '0') const gas = usdcFees ? usdcFees.baseFee + usdcFees.gasFees : BigInt(Number.MAX_SAFE_INTEGER) const hasEnoughGas = @@ -180,6 +162,7 @@ export function SendConfirm() { assert(nonce !== undefined, 'Nonce is not available') throwIf(feesPerGasError) assert(!!feesPerGas, 'Fees per gas is not available') + assert(!!profile?.address, 'Could not resolve recipients send account') assert(selectedCoin?.balance >= BigInt(amount ?? '0'), 'Insufficient balance') const sender = sendAccount?.address as `0x${string}` @@ -190,15 +173,38 @@ export function SendConfirm() { maxPriorityFeePerGas: feesPerGas.maxPriorityFeePerGas, } - log('gasEstimate', usdcFees) - log('feesPerGas', feesPerGas) - log('userOp', _userOp) - const receipt = await sendUserOp({ - userOp: _userOp, - webauthnCreds, + console.log('gasEstimate', usdcFees) + console.log('feesPerGas', feesPerGas) + console.log('userOp', _userOp) + const chainId = baseMainnetClient.chain.id + const entryPoint = entryPointAddress[chainId] + const userOpHash = getUserOperationHash({ + userOperation: userOp, + entryPoint, + chainId, + }) + const signature = await signUserOp({ + userOpHash, + allowedCredentials: + webauthnCreds?.map((c) => ({ + id: byteaToBase64(c.raw_credential_id), + userHandle: c.name, + })) ?? [], + }) + userOp.signature = signature + + const { data: workflowId, error } = await transfer({ + token: selectedCoin.token, + userOp, + }).catch((e) => { + console.error("Couldn't send the userOp", e) + if (e instanceof TRPCClientError) { + return { data: undefined, error: { message: e.message } } + } + return { data: undefined, error: { message: e.message } } }) - assert(receipt.success, 'Failed to send user op') - setSentTxHash(receipt.receipt.transactionHash) + console.log('workflowId', workflowId) + console.log('error', error) if (selectedCoin?.token === 'eth') { await ethQuery.refetch() } else { @@ -211,62 +217,6 @@ export function SendConfirm() { } } - useEffect(() => { - if (!submittedAt) return - - const hasBeenLongEnough = Date.now() - submittedAt > 5_000 - - log('check if submitted at is long enough', { - submittedAt, - sentTxHash, - hasBeenLongEnough, - isTransferPending, - }) - - if (sentTxHash) { - log('sent tx hash', { sentTxHash }) - const tfr = transfers?.pages.some((page) => - page.some((activity: Activity) => { - if (isSendAccountTransfersEvent(activity)) { - return activity.data.tx_hash === sentTxHash - } - if (isSendAccountReceiveEvent(activity)) { - return activity.data.tx_hash === sentTxHash - } - return false - }) - ) - - if (tokenActivityError) { - console.error(tokenActivityError) - } - // found the transfer or we waited too long or we got an error 😢 - // or we are sending eth since event logs are not always available for eth - // (when receipient is not a send account or contract) - if (tfr || tokenActivityError || hasBeenLongEnough || (sentTxHash && sendToken === 'eth')) { - router.replace({ pathname: '/', query: { token: sendToken } }) - } - } - - // create a window unload event on web - const eventHandlersToRemove: (() => void)[] = [] - if (isWeb) { - const unloadHandler = (e: BeforeUnloadEvent) => { - // prevent unload if we have a tx hash or a submitted at - if (submittedAt || sentTxHash) { - e.preventDefault() - } - } - window.addEventListener('beforeunload', unloadHandler) - eventHandlersToRemove.push(() => window.removeEventListener('beforeunload', unloadHandler)) - } - - return () => { - for (const remove of eventHandlersToRemove) { - remove() - } - } - }, [sentTxHash, transfers, router, sendToken, tokenActivityError, submittedAt, isTransferPending]) if (isSendAccountLoading || nonceIsLoading || isProfileLoading) return @@ -394,7 +344,7 @@ export function SendConfirm() { onPress={onSubmit} br={'$4'} disabledStyle={{ opacity: 0.7, cursor: 'not-allowed', pointerEvents: 'none' }} - disabled={!canSubmit || isTransferPending || !!sentTxHash} + disabled={!canSubmit || isTransferPending} gap={4} py={'$5'} width={'100%'} @@ -416,15 +366,6 @@ export function SendConfirm() { Sending... ) - case sentTxHash !== undefined: - return ( - <> - - - - Confirming... - - ) case !hasEnoughBalance: return Insufficient Balance case !hasEnoughGas: diff --git a/packages/app/features/send/screen.test.tsx b/packages/app/features/send/screen.test.tsx index a894bbbfa..f6f270fe4 100644 --- a/packages/app/features/send/screen.test.tsx +++ b/packages/app/features/send/screen.test.tsx @@ -6,6 +6,16 @@ jest.mock('expo-router', () => ({ usePathname: jest.fn().mockReturnValue('/send'), })) +jest.mock('app/utils/api', () => ({ + transfer: { + withUserOp: jest.fn().mockReturnValue({ + useMutation: jest.fn().mockReturnValue({ + mutateAsync: jest.fn().mockReturnValue(Promise.resolve('123')), + }), + }), + }, +})) + jest.mock('app/provider/coins', () => ({ useCoins: jest.fn().mockReturnValue({ coins: [ diff --git a/packages/app/utils/useUserOpTransferMutation.ts b/packages/app/utils/useUserOpTransferMutation.ts index 204fbfae2..87d5864c9 100644 --- a/packages/app/utils/useUserOpTransferMutation.ts +++ b/packages/app/utils/useUserOpTransferMutation.ts @@ -136,6 +136,7 @@ export async function sendUserOpTransfer({ throwNiceError(e) } } + export function useGenerateTransferUserOp({ sender, to, From b7e62d68e7b7d3813ea6467b74e98a5ccb8e1ea7 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 28 Aug 2024 17:57:55 -0700 Subject: [PATCH 05/15] Create transfer temporal workflow --- apps/workers/src/worker.ts | 2 +- .../src/distribution-workflow/activities.ts | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index 71ec4354b..e93cd7599 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -36,7 +36,7 @@ async function run() { // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls // Step 2: Start accepting tasks on the `monorepo` queue - await worker.run() + await transferWorker.run() // You may create multiple Workers in a single process in order to poll on multiple task queues. } diff --git a/packages/workflows/src/distribution-workflow/activities.ts b/packages/workflows/src/distribution-workflow/activities.ts index 1db8f8a34..93b4a3df3 100644 --- a/packages/workflows/src/distribution-workflow/activities.ts +++ b/packages/workflows/src/distribution-workflow/activities.ts @@ -10,6 +10,7 @@ import { } from './supabase' import { fetchAllBalances, isMerkleDropActive } from './wagmi' import { calculatePercentageWithBips, calculateWeights, PERC_DENOM } from './weights' +import { bootstrap } from '@my/workflows/utils' const cpuCount = cpus().length @@ -19,16 +20,8 @@ const inBatches = (array: T[], batchSize = Math.max(8, cpuCount - 1)) => { ) } -export function createDistributionActivities(supabaseUrl: string, supabaseKey: string) { - globalThis.process = globalThis.process || {} - globalThis.process.env.SUPABASE_URL = supabaseUrl // HACK: set the supabase url in the environment - globalThis.process.env.SUPABASE_SERVICE_ROLE = supabaseKey // HACK: set the supabase key in the environment - - return { - calculateDistributionSharesActivity, - fetchDistributionActivity, - fetchAllOpenDistributionsActivity, - } +export function createDistributionActivities(env: Record) { + bootstrap(env) } async function fetchAllOpenDistributionsActivity() { From 7a024c8baa35db6b3d5cbcbc25762be1f8b6dd50 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Mon, 2 Sep 2024 16:17:18 -0700 Subject: [PATCH 06/15] Encapsulate temporal into a package --- apps/workers/src/worker.ts | 2 +- apps/workers/tsconfig.json | 3 ++- packages/workflows/package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index e93cd7599..71ec4354b 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -36,7 +36,7 @@ async function run() { // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls // Step 2: Start accepting tasks on the `monorepo` queue - await transferWorker.run() + await worker.run() // You may create multiple Workers in a single process in order to poll on multiple task queues. } diff --git a/apps/workers/tsconfig.json b/apps/workers/tsconfig.json index 487ce956d..6ad8297a0 100644 --- a/apps/workers/tsconfig.json +++ b/apps/workers/tsconfig.json @@ -21,6 +21,7 @@ "./src", "../../packages/workflows/src", "../../globals.d.ts", - "../../environment.d.ts" + "../../environment.d.ts", + "../../packages/temporal/src" ] } diff --git a/packages/workflows/package.json b/packages/workflows/package.json index ac11b461c..3f52db670 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -5,7 +5,7 @@ "src" ], "exports": { - "./all-activities": { + "./activities": { "types": "./src/all-activities.ts", "default": "./src/all-activities.ts" }, From 96f053ce095bf0201ff44357d7bcc04a0cee54cf Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 28 Aug 2024 17:57:55 -0700 Subject: [PATCH 07/15] Create transfer tempora workflow --- apps/workers/src/worker.ts | 6 +++++- .../workflows/src/transfer-workflow/supabase.ts | 1 - .../workflows/src/transfer-workflow/workflow.ts | 13 ++++--------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index 71ec4354b..e9daa07f5 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -7,6 +7,10 @@ import { URL, fileURLToPath } from 'node:url' import path from 'node:path' async function run() { + const workflowsPathUrl = new URL( + `../../../packages/workflows/src/all-workflows${path.extname(import.meta.url)}`, + import.meta.url + ) // Step 1: Register Workflows and Activities with the Worker and connect to // the Temporal server. const transferWorker = await Worker.create({ @@ -36,7 +40,7 @@ async function run() { // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls // Step 2: Start accepting tasks on the `monorepo` queue - await worker.run() + await transferWorker.run() // You may create multiple Workers in a single process in order to poll on multiple task queues. } diff --git a/packages/workflows/src/transfer-workflow/supabase.ts b/packages/workflows/src/transfer-workflow/supabase.ts index 1760f1219..4842eea95 100644 --- a/packages/workflows/src/transfer-workflow/supabase.ts +++ b/packages/workflows/src/transfer-workflow/supabase.ts @@ -1,4 +1,3 @@ -import { log, ApplicationFailure } from '@temporalio/activity' import { hexToBytea } from 'app/utils/hexToBytea' import { supabaseAdmin } from 'app/utils/supabase/admin' diff --git a/packages/workflows/src/transfer-workflow/workflow.ts b/packages/workflows/src/transfer-workflow/workflow.ts index 340a210d6..d1d5535f9 100644 --- a/packages/workflows/src/transfer-workflow/workflow.ts +++ b/packages/workflows/src/transfer-workflow/workflow.ts @@ -2,15 +2,10 @@ import { proxyActivities, ApplicationFailure, defineQuery, setHandler } from '@t import type { createTransferActivities } from './activities' import type { UserOperation, GetUserOperationReceiptReturnType } from 'permissionless' -const { - simulateUserOpActivity, - sendUserOpActivity, - waitForTransactionReceiptActivity, - isTransferIndexedActivity, -} = proxyActivities>({ - // TODO: make this configurable - startToCloseTimeout: '45 seconds', -}) +const { sendUserOpActivity, waitForTransactionReceiptActivity, fetchTransferActivity } = + proxyActivities>({ + startToCloseTimeout: '30 seconds', + }) type simulating = { status: 'simulating'; data: { userOp: UserOperation<'v0.7'> } } type sending = { status: 'sending'; data: { userOp: UserOperation<'v0.7'> } } From 99b00943f251daa5cd227691e65f6429547afbd2 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Mon, 2 Sep 2024 16:17:18 -0700 Subject: [PATCH 08/15] Encapsulate temporal into a package --- apps/workers/src/worker.ts | 6 +----- .../workflows/src/transfer-workflow/supabase.ts | 1 + .../workflows/src/transfer-workflow/workflow.ts | 13 +++++++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index e9daa07f5..71ec4354b 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -7,10 +7,6 @@ import { URL, fileURLToPath } from 'node:url' import path from 'node:path' async function run() { - const workflowsPathUrl = new URL( - `../../../packages/workflows/src/all-workflows${path.extname(import.meta.url)}`, - import.meta.url - ) // Step 1: Register Workflows and Activities with the Worker and connect to // the Temporal server. const transferWorker = await Worker.create({ @@ -40,7 +36,7 @@ async function run() { // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls // Step 2: Start accepting tasks on the `monorepo` queue - await transferWorker.run() + await worker.run() // You may create multiple Workers in a single process in order to poll on multiple task queues. } diff --git a/packages/workflows/src/transfer-workflow/supabase.ts b/packages/workflows/src/transfer-workflow/supabase.ts index 4842eea95..1760f1219 100644 --- a/packages/workflows/src/transfer-workflow/supabase.ts +++ b/packages/workflows/src/transfer-workflow/supabase.ts @@ -1,3 +1,4 @@ +import { log, ApplicationFailure } from '@temporalio/activity' import { hexToBytea } from 'app/utils/hexToBytea' import { supabaseAdmin } from 'app/utils/supabase/admin' diff --git a/packages/workflows/src/transfer-workflow/workflow.ts b/packages/workflows/src/transfer-workflow/workflow.ts index d1d5535f9..340a210d6 100644 --- a/packages/workflows/src/transfer-workflow/workflow.ts +++ b/packages/workflows/src/transfer-workflow/workflow.ts @@ -2,10 +2,15 @@ import { proxyActivities, ApplicationFailure, defineQuery, setHandler } from '@t import type { createTransferActivities } from './activities' import type { UserOperation, GetUserOperationReceiptReturnType } from 'permissionless' -const { sendUserOpActivity, waitForTransactionReceiptActivity, fetchTransferActivity } = - proxyActivities>({ - startToCloseTimeout: '30 seconds', - }) +const { + simulateUserOpActivity, + sendUserOpActivity, + waitForTransactionReceiptActivity, + isTransferIndexedActivity, +} = proxyActivities>({ + // TODO: make this configurable + startToCloseTimeout: '45 seconds', +}) type simulating = { status: 'simulating'; data: { userOp: UserOperation<'v0.7'> } } type sending = { status: 'sending'; data: { userOp: UserOperation<'v0.7'> } } From ab064f315c2ae2ebbf6caa08accdea438ecfc935 Mon Sep 17 00:00:00 2001 From: Beezy Date: Tue, 1 Oct 2024 03:10:34 +0000 Subject: [PATCH 09/15] Temporal cloud changes --- Makefile | 62 +++++++++++ apps/next/Dockerfile | 1 + apps/next/package.json | 2 +- apps/next/tsconfig.json | 3 +- apps/workers/src/client.ts | 4 +- apps/workers/src/worker.ts | 56 +++++----- package.json | 2 +- .../src/routers/account-recovery/router.ts | 2 +- packages/api/src/routers/transfer.ts | 24 ++-- packages/api/tsconfig.json | 3 +- packages/temporal/package.json | 4 + packages/temporal/src/client.ts | 51 +++++++++ packages/temporal/src/payload-converter.ts | 7 ++ .../src/superjson-payload-converter.ts | 83 ++++++++++++++ packages/workflows/package.json | 8 ++ .../src/transfer-workflow/activities.ts | 105 +++++++++--------- .../src/transfer-workflow/supabase.ts | 1 + .../workflows/src/transfer-workflow/wagmi.ts | 28 +++-- .../src/transfer-workflow/workflow.ts | 14 ++- packages/workflows/tsconfig.json | 3 +- yarn.lock | 61 +++++----- 21 files changed, 382 insertions(+), 142 deletions(-) create mode 100644 Makefile create mode 100644 packages/temporal/src/client.ts create mode 100644 packages/temporal/src/payload-converter.ts create mode 100644 packages/temporal/src/superjson-payload-converter.ts diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a1563864a --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# Makefile + +# Check if .env.local exists, if not, create it from template +$(shell test -f .env.local || cp .env.local.template .env.local) +include .env.local + +# Export variables from .env.local if not already set in the environment +define read_env + $(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$//; s/^/export /; s/$$/?=$$\(shell grep -m1 "^&=" .env.local | cut -d= -f2-\)/' .env.local)) +endef + +$(call read_env) + +IMAGE_NAME = sendapp/next-app +GIT_BRANCH = $(shell git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD) +GIT_HASH = $(shell git rev-parse --short=10 HEAD) +NEXT_COMPOSE_IMAGE = "${IMAGE_NAME}-${GIT_BRANCH}-${GIT_HASH}" +DOCKERFILE_PATH = ./apps/next/Dockerfile +BUILD_CONTEXT = . + +# Docker build arguments +BUILD_ARGS = \ + --build-arg CI=${CI} \ + --build-arg DEBUG=${DEBUG} \ + --build-arg NEXT_PUBLIC_SUPABASE_PROJECT_ID=${NEXT_PUBLIC_SUPABASE_PROJECT_ID} \ + --build-arg NEXT_PUBLIC_URL=${NEXT_PUBLIC_URL} \ + --build-arg NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} \ + --build-arg NEXT_PUBLIC_SUPABASE_GRAPHQL_URL=${NEXT_PUBLIC_SUPABASE_GRAPHQL_URL} \ + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} \ + --build-arg NEXT_PUBLIC_MAINNET_RPC_URL=${NEXT_PUBLIC_MAINNET_RPC_URL} \ + --build-arg NEXT_PUBLIC_BASE_RPC_URL=${NEXT_PUBLIC_BASE_RPC_URL} \ + --build-arg NEXT_PUBLIC_BUNDLER_RPC_URL=${NEXT_PUBLIC_BUNDLER_RPC_URL} \ + --build-arg NEXT_PUBLIC_MAINNET_CHAIN_ID=${NEXT_PUBLIC_MAINNET_CHAIN_ID} \ + --build-arg NEXT_PUBLIC_BASE_CHAIN_ID=${NEXT_PUBLIC_BASE_CHAIN_ID} \ + --build-arg NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=${NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID} \ + --build-arg NEXT_PUBLIC_TURNSTILE_SITE_KEY=${NEXT_PUBLIC_TURNSTILE_SITE_KEY} + +# Docker secrets +SECRETS = \ + --secret id=SUPABASE_DB_URL \ + --secret id=SUPABASE_SERVICE_ROLE \ + --secret id=TURBO_TOKEN \ + --secret id=TURBO_TEAM + + +# Targets +.PHONY: docker-web docker-web-push +docker-web: + @if [ -z "$$(docker images -q $(NEXT_COMPOSE_IMAGE))" ]; then \ + echo "Image $(NEXT_COMPOSE_IMAGE) does not exist locally. Building..."; \ + docker buildx build --progress=plain --platform linux/amd64 -t $(IMAGE_NAME)-$(GIT_BRANCH):$(GIT_HASH) -t $(IMAGE_NAME)-$(GIT_BRANCH):latest $(BUILD_ARGS) $(SECRETS) -f $(DOCKERFILE_PATH) $(BUILD_CONTEXT) ;\ + else \ + echo "Image $(NEXT_COMPOSE_IMAGE) already exists locally. Skipping build."; \ + fi + +docker-web-push: docker-web + docker push $(IMAGE_NAME)-$(GIT_BRANCH):$(GIT_HASH) + docker push $(IMAGE_NAME)-$(GIT_BRANCH):latest + +# Prune docker system images and containers older than 7 days != otterscan +docker-clean: + docker image prune -f --filter "label!=otterscan*" --filter until=168h diff --git a/apps/next/Dockerfile b/apps/next/Dockerfile index 8356a291e..35e7685ee 100644 --- a/apps/next/Dockerfile +++ b/apps/next/Dockerfile @@ -32,6 +32,7 @@ COPY packages/eslint-config-custom/package.json packages/eslint-config-custom/pa COPY packages/playwright/package.json packages/playwright/package.json COPY packages/shovel/package.json packages/shovel/package.json COPY packages/snaplet/package.json packages/snaplet/package.json +COPY packages/temporal/package.json packages/temporal/package.json COPY packages/ui/package.json packages/ui/package.json COPY packages/wagmi/package.json packages/wagmi/package.json COPY packages/webauthn-authenticator/package.json packages/webauthn-authenticator/package.json diff --git a/apps/next/package.json b/apps/next/package.json index 65c27f771..f8cd50bd3 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -11,7 +11,7 @@ "serve": "NODE_ENV=production yarn with-env next start --port 8151", "lint": "next lint", "lint:fix": "next lint --fix", - "with-env": "TAMAGUI_TARGET=web dotenv -e ../../.env -c --" + "with-env": "TAMAGUI_TARGET=web dotenv -e ../../.env.localtemp -c --" }, "dependencies": { "@my/api": "workspace:*", diff --git a/apps/next/tsconfig.json b/apps/next/tsconfig.json index 4191fcaf3..204f3e3c7 100644 --- a/apps/next/tsconfig.json +++ b/apps/next/tsconfig.json @@ -30,7 +30,8 @@ "pathToApp": "." } ], - "types": ["node"] + "types": ["node"], + "sourceMap": true }, "include": [ "next-env.d.ts", diff --git a/apps/workers/src/client.ts b/apps/workers/src/client.ts index f3eb01622..6fe55a7ab 100644 --- a/apps/workers/src/client.ts +++ b/apps/workers/src/client.ts @@ -25,13 +25,13 @@ import type { UserOperation } from 'permissionless' // return result // } -async function runTransferWorkflow(userOp: UserOperation<'v0.7'>) { +export async function runTransferWorkflow(userOp: UserOperation<'v0.7'>) { const connection = await Connection.connect() const client = new Client({ connection, }) - const handle = await client.workflow.start(TransferWorkflow, { + const handle = await client.workflow.start(SendTransferWorkflow, { taskQueue: 'monorepo', workflowId: `transfers-workflow-${userOp.sender}-${userOp.nonce.toString()}`, // TODO: remember to replace this with a meaningful business ID args: [userOp], diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index 71ec4354b..151de0bbc 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -1,25 +1,41 @@ -import { Worker } from '@temporalio/worker' +import { Worker, NativeConnection } from '@temporalio/worker' import { createTransferActivities, createDistributionActivities, } from '@my/workflows/all-activities' -import { URL, fileURLToPath } from 'node:url' -import path from 'node:path' +import fs from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dataConverter } from '@my/temporal/payload-converter' +const require = createRequire(import.meta.url) + +const { NODE_ENV = 'development' } = process.env +const isDeployed = ['production', 'staging'].includes(NODE_ENV) async function run() { - // Step 1: Register Workflows and Activities with the Worker and connect to - // the Temporal server. - const transferWorker = await Worker.create({ - workflowsPath: fileURLToPath(workflowsPathUrl), + const connection = isDeployed + ? await NativeConnection.connect({ + address: `${process.env.TEMPORAL_NAMESPACE}.tmprl.cloud:7233`, + tls: { + clientCertPair: { + crt: await fs.readFile(process.env.TEMPORAL_MTLS_TLS_CERT ?? '').catch((e) => { + console.error(e) + throw new Error('no cert found. Check the TEMPORAL_MTLS_TLS_CERT env var') + }), + key: await fs.readFile(process.env.TEMPORAL_MTLS_TLS_KEY ?? '').catch((e) => { + console.error(e) + throw new Error('no key found. Check the TEMPORAL_MTLS_TLS_KEY env var') + }), + }, + }, + }) + : undefined + + const worker = await Worker.create({ + connection, + dataConverter: dataConverter, + workflowsPath: require.resolve('@my/workflows'), activities: { - ...createTransferActivities( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.SUPABASE_SERVICE_ROLE - ), - // ...createDistributionActivities( - // process.env.NEXT_PUBLIC_SUPABASE_URL, - // process.env.SUPABASE_SERVICE_ROLE - // ), + ...createTransferActivities(process.env), }, namespace: 'default', taskQueue: 'monorepo', @@ -28,17 +44,7 @@ async function run() { }, }) - // Worker connects to localhost by default and uses console.error for logging. - // Customize the Worker by passing more options to create(): - // https://typescript.temporal.io/api/classes/worker.Worker - - // If you need to configure server connection parameters, see the mTLS example: - // https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls - - // Step 2: Start accepting tasks on the `monorepo` queue await worker.run() - - // You may create multiple Workers in a single process in order to poll on multiple task queues. } run().catch((err) => { diff --git a/package.json b/package.json index a8bc7bc9b..71b32a100 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "check-dependency-version-consistency": "^3.0.3", "eslint": "^8.46.0", "node-gyp": "^9.3.1", - "turbo": "^2.0.3", + "turbo": "^2.1.2", "typescript": "^5.5.3" }, "packageManager": "yarn@4.3.1", diff --git a/packages/api/src/routers/account-recovery/router.ts b/packages/api/src/routers/account-recovery/router.ts index 2c0247908..95ec44556 100644 --- a/packages/api/src/routers/account-recovery/router.ts +++ b/packages/api/src/routers/account-recovery/router.ts @@ -50,7 +50,7 @@ export const accountRecoveryRouter = createTRPCRouter({ .single() if (challengeError || !challengeData) { - logger(`getChallenge:cant-insert-challenge: [${challengeError}]`) + logger(`getChallenge:cant-insert-challenge: [${JSON.stringify(challengeError)}]`) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: formatErr('Cannot generate challenge: Internal server error'), diff --git a/packages/api/src/routers/transfer.ts b/packages/api/src/routers/transfer.ts index 5c7ece373..be70743cc 100644 --- a/packages/api/src/routers/transfer.ts +++ b/packages/api/src/routers/transfer.ts @@ -2,8 +2,9 @@ import { TRPCError } from '@trpc/server' import debug from 'debug' import { z } from 'zod' import { createTRPCRouter, protectedProcedure } from '../trpc' -import { client } from '@my/temporal/client' +import { getTemporalClient } from '@my/temporal/client' import type { UserOperation } from 'permissionless' + import { TransferWorkflow, type transferState } from '@my/workflows' import type { allCoins } from 'app/data/coins' @@ -20,14 +21,14 @@ export const transferRouter = createTRPCRouter({ .mutation(async ({ input: { token, userOp } }) => { const { sender, nonce } = userOp try { + const client = await getTemporalClient() const handle = await client.workflow.start(TransferWorkflow, { taskQueue: 'monorepo', - workflowId: `transfer-workflow-${token}-${sender}-${nonce}`, + workflowId: `send-transfer-workflow-${token}-${sender}-${nonce}`, args: [userOp], }) - log('Started transfer handle', handle.workflowId) - // optional: wait for client result - return await handle.workflowId + log(`Workflow Created: ${handle.workflowId}`) + return handle.workflowId } catch (error) { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', @@ -37,7 +38,8 @@ export const transferRouter = createTRPCRouter({ }), getState: protectedProcedure.input(z.string()).query(async ({ input: workflowId }) => { try { - const handle = await client.workflow.getHandle(workflowId) + const client = await getTemporalClient() + const handle = client.workflow.getHandle(workflowId) const state = await handle.query('getTransferState') return state } catch (error) { @@ -57,11 +59,12 @@ export const transferRouter = createTRPCRouter({ .query(async ({ input: { token, sender } }) => { try { const states: transferState[] = [] - const workflows = await client.workflow.list({ + const client = await getTemporalClient() + const workflows = client.workflow.list({ query: `ExecutionStatus = "Running" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`, }) for await (const workflow of workflows) { - const handle = await client.workflow.getHandle(workflow.workflowId) + const handle = client.workflow.getHandle(workflow.workflowId) const state = await handle.query('getTransferState') states.push(state) @@ -84,11 +87,12 @@ export const transferRouter = createTRPCRouter({ .query(async ({ input: { token, sender } }) => { try { const states: transferState[] = [] - const workflows = await client.workflow.list({ + const client = await getTemporalClient() + const workflows = client.workflow.list({ query: `ExecutionStatus = "Failed" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`, }) for await (const workflow of workflows) { - const handle = await client.workflow.getHandle(workflow.workflowId) + const handle = client.workflow.getHandle(workflow.workflowId) const state = await handle.query('getTransferState') states.push(state) } diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 845dd7939..d2e8cdc17 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -9,7 +9,8 @@ "app/*": ["../app/*"], "@my/wagmi": ["../wagmi/src"], "@my/wagmi/*": ["../wagmi/src/*"], - "@my/supabase/*": ["../../supabase/*"] + "@my/supabase/*": ["../../supabase/*"], + "@my/workflows": ["./packages/workflows/src/all-workflows.ts"] } }, "references": [] diff --git a/packages/temporal/package.json b/packages/temporal/package.json index d47c437c3..ce6dd2db2 100644 --- a/packages/temporal/package.json +++ b/packages/temporal/package.json @@ -15,6 +15,10 @@ "./client": { "types": "./src/client.ts", "default": "./src/client.ts" + }, + "./superjson-payload-converter": { + "types": "./src/superjson-payload-converter.ts", + "default": "./src/superjson-payload-converter.ts" } }, "scripts": { diff --git a/packages/temporal/src/client.ts b/packages/temporal/src/client.ts new file mode 100644 index 000000000..298224e21 --- /dev/null +++ b/packages/temporal/src/client.ts @@ -0,0 +1,51 @@ +import { Client, Connection } from '@temporalio/client' +import { dataConverter } from './payload-converter' +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) +import debug from 'debug' +import fs from 'node:fs/promises' +const { NODE_ENV = 'development' } = process.env +const isDeployed = ['production', 'staging'].includes(NODE_ENV) + +const log = debug('api:temporal') +log(`connecting to temporal: ${process.env.TEMPORAL_NAMESPACE} with NODE_ENV: ${NODE_ENV}`) + +let connectionOptions = {} +if (isDeployed) { + connectionOptions = { + address: `${process.env.TEMPORAL_NAMESPACE}.tmprl.cloud:7233`, + tls: { + clientCertPair: { + crt: await fs.readFile(process.env.TEMPORAL_MTLS_TLS_CERT ?? '').catch((e) => { + console.error(e) + throw new Error('no cert found. Check the TEMPORAL_MTLS_TLS_CERT env var') + }), + key: await fs.readFile(process.env.TEMPORAL_MTLS_TLS_KEY ?? '').catch((e) => { + console.error(e) + throw new Error('no key found. Check the TEMPORAL_MTLS_TLS_KEY env var') + }), + }, + }, + } +} + +let client: Client | null = null + +export async function getTemporalClient(): Promise { + if (!client) { + const connection = await Connection.connect(connectionOptions) + client = new Client({ + connection, + namespace: process.env.TEMPORAL_NAMESPACE ?? 'default', + dataConverter: dataConverter, + }) + } + return client +} + +export async function closeTemporalClient(): Promise { + if (client) { + await client.connection.close() + client = null + } +} diff --git a/packages/temporal/src/payload-converter.ts b/packages/temporal/src/payload-converter.ts new file mode 100644 index 000000000..fc1cd19d6 --- /dev/null +++ b/packages/temporal/src/payload-converter.ts @@ -0,0 +1,7 @@ +import { CompositePayloadConverter, UndefinedPayloadConverter } from '@temporalio/common' +import { SuperjsonPayloadConverter } from './superjson-payload-converter' + +export const payloadConverter = new CompositePayloadConverter( + new UndefinedPayloadConverter(), + new SuperjsonPayloadConverter() +) diff --git a/packages/temporal/src/superjson-payload-converter.ts b/packages/temporal/src/superjson-payload-converter.ts new file mode 100644 index 000000000..3ab6367d4 --- /dev/null +++ b/packages/temporal/src/superjson-payload-converter.ts @@ -0,0 +1,83 @@ +import { + type EncodingType, + METADATA_ENCODING_KEY, + type Payload, + type PayloadConverterWithEncoding, + PayloadConverterError, +} from '@temporalio/common' +import superjson from 'superjson' +import { decode, encode } from '@temporalio/common/lib/encoding' + +/** + * Converts between values and [SUPERJSON](https://github.com/flightcontrolhq/superjson) Payloads. + */ +export class SuperjsonPayloadConverter implements PayloadConverterWithEncoding { + // Use 'json/plain' so that Payloads are displayed in the UI + public encodingType = 'json/plain' as EncodingType + + public toPayload(value: unknown): Payload | undefined { + if (value === undefined) return undefined + let sjson = '' + try { + sjson = superjson.stringify(value) + } catch (e) { + throw new UnsupportedSuperjsonTypeError( + `Can't run SUPERJSON.stringify on this value: ${value}. Either convert it (or its properties) to SUPERJSON-serializable values (see https://github.com/flightcontrolhq/superjson#readme ), or create a custom data converter. SJSON.stringify error message: ${errorMessage( + e + )}`, + e as Error + ) + } + + return { + metadata: { + [METADATA_ENCODING_KEY]: encode('json/plain'), + // Include an additional metadata field to indicate that this is an SuperJSON payload + format: encode('extended'), + }, + data: encode(sjson), + } + } + + public fromPayload(content: Payload): T { + try { + if (!content.data) { + throw new UnsupportedSuperjsonTypeError( + `Can't run SUPERJSON.parse on this value: ${content.data}. Either convert it (or its properties) to SUPERJSON-serializable values (see https://github.com/flightcontrolhq/superjson#readme ), or create a custom data converter. No data found in payload.` + ) + } + return superjson.parse(decode(content.data)) + } catch (e) { + throw new UnsupportedSuperjsonTypeError( + `Can't run SUPERJSON.parse on this value: ${ + content.data + }. Either convert it (or its properties) to SUPERJSON-serializable values (see https://github.com/flightcontrolhq/superjson#readme ), or create a custom data converter. SJSON.parse error message: ${errorMessage( + e + )}`, + e as Error + ) + } + } +} + +export class UnsupportedSuperjsonTypeError extends PayloadConverterError { + public readonly name: string = 'UnsupportedJsonTypeError' + + constructor( + message: string | undefined, + public readonly cause?: Error + ) { + super(message ?? undefined) + } +} +// @@@SNIPEND + +export function errorMessage(error: unknown): string | undefined { + if (typeof error === 'string') { + return error + } + if (error instanceof Error) { + return error.message + } + return undefined +} diff --git a/packages/workflows/package.json b/packages/workflows/package.json index 3f52db670..af90b30c5 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -9,9 +9,17 @@ "types": "./src/all-activities.ts", "default": "./src/all-activities.ts" }, + "./utils": { + "types": "./src/utils/index.ts", + "default": "./src/utils/index.ts" + }, "./all-workflows": { "types": "./src/all-workflows.ts", "default": "./src/all-workflows.ts" + }, + "/": { + "types": "./src/all-workflows.ts", + "default": "./src/all-workflows.ts" } }, "type": "module", diff --git a/packages/workflows/src/transfer-workflow/activities.ts b/packages/workflows/src/transfer-workflow/activities.ts index cd662fd78..67581eb37 100644 --- a/packages/workflows/src/transfer-workflow/activities.ts +++ b/packages/workflows/src/transfer-workflow/activities.ts @@ -1,58 +1,60 @@ import { log, ApplicationFailure } from '@temporalio/activity' -import { fetchTransfer } from './supabase' -import { - simulateUserOperation, - sendUserOperation, - waitForTransactionReceipt, - generateTransferUserOp, -} from './wagmi' -import type { TransferWorkflowArgs } from './workflow' -import { isAddress, type Hex } from 'viem' -export function createTransferActivities(supabaseUrl: string, supabaseKey: string) { - globalThis.process = globalThis.process || {} - globalThis.process.env.SUPABASE_URL = supabaseUrl // HACK: set the supabase url in the environment - globalThis.process.env.SUPABASE_SERVICE_ROLE = supabaseKey // HACK: set the supabase key in the environment +import { isTransferIndexed } from './supabase' +import { simulateUserOperation, sendUserOperation, waitForTransactionReceipt } from './wagmi' +import type { UserOperation } from 'permissionless' +import { bootstrap } from '@my/workflows/utils' + +export const createTransferActivities = (env: Record) => { + bootstrap(env) return { + simulateUserOpActivity, sendUserOpActivity, - fetchTransferActivity, waitForTransactionReceiptActivity, + isTransferIndexedActivity, } } - -async function sendUserOpActivity(args: TransferWorkflowArgs) { - const { sender, to, token, amount, nonce } = args - const parsedAmount = BigInt(amount) - const parsedNonce = BigInt(nonce) - if (!!sender && !isAddress(sender)) - throw ApplicationFailure.nonRetryable('Invalid send account address') - if (!!to && !isAddress(to)) throw ApplicationFailure.nonRetryable('Invalid to address') - if (!token || !isAddress(token)) throw ApplicationFailure.nonRetryable('Invalid token address') - if (typeof parsedAmount !== 'bigint' || parsedAmount <= 0n) - throw ApplicationFailure.nonRetryable('Invalid amount') - if (typeof parsedNonce !== 'bigint' || parsedNonce < 0n) - throw ApplicationFailure.nonRetryable('Invalid nonce') +async function simulateUserOpActivity(userOp: UserOperation<'v0.7'>) { + if (!userOp.signature) { + throw ApplicationFailure.nonRetryable('UserOp signature is required') + } try { - const userOp = await generateTransferUserOp({ - sender, - to, - token, - amount: parsedAmount, - nonce: parsedNonce, - }) - userOp.signature = args.signature - console.log('userOp: ', userOp) + await simulateUserOperation(userOp) + } catch (error) { + throw ApplicationFailure.nonRetryable('Error simulating user operation', error.code, error) + } +} + +async function sendUserOpActivity(userOp: UserOperation<'v0.7'>) { + const creationTime = Date.now() + try { const hash = await sendUserOperation(userOp) - console.log('hash: ', hash) - log.info('sendUserOperationActivity', { hash, userOp }) + log.info('UserOperation sent', { + hash, + sendTime: Date.now(), + userOp: JSON.stringify(userOp, null, 2), + }) return hash } catch (error) { - throw ApplicationFailure.nonRetryable('Error sending user operation', error.code, error) + const errorMessage = + error instanceof Error ? `${error.name}: ${error.message}` : 'Unknown error occurred' + + log.error('Error in sendUserOpActivity', { + error: errorMessage, + creationTime, + sendTime: Date.now(), + userOp: JSON.stringify(userOp, null, 2), + }) + + throw ApplicationFailure.nonRetryable(errorMessage) } } async function waitForTransactionReceiptActivity(hash: `0x${string}`) { + if (!hash) { + throw ApplicationFailure.nonRetryable('Invalid hash: hash is undefined') + } try { const receipt = await waitForTransactionReceipt(hash) if (!receipt.success) @@ -60,23 +62,16 @@ async function waitForTransactionReceiptActivity(hash: `0x${string}`) { log.info('waitForTransactionReceiptActivity', { receipt }) return receipt } catch (error) { - throw ApplicationFailure.nonRetryable('Error waiting for tx receipt', error.code, error) + const errorMessage = error instanceof Error ? error.message : String(error) + log.error('Error in waitForTransactionReceiptActivity', { hash, error: errorMessage }) + throw ApplicationFailure.nonRetryable('Error waiting for tx receipt', errorMessage) } } - -async function fetchTransferActivity(hash: `0x${string}`) { - const { data: transfer, error } = await fetchTransfer(hash) - if (error) { - if (error.code === 'PGRST116') { - log.info('fetchTransferActivity', { error }) - return null - } - throw ApplicationFailure.nonRetryable( - 'Error fetching transfer from activity column.', - error.code, - error - ) +async function isTransferIndexedActivity(hash: `0x${string}`) { + const isIndexed = await isTransferIndexed(hash) + log.info('isTransferIndexedActivity', { isIndexed }) + if (!isIndexed) { + throw ApplicationFailure.retryable('Transfer not yet indexed in db') } - log.info('fetchTransferActivity', { transfer }) - return transfer + return isIndexed } diff --git a/packages/workflows/src/transfer-workflow/supabase.ts b/packages/workflows/src/transfer-workflow/supabase.ts index 1760f1219..94c0352e0 100644 --- a/packages/workflows/src/transfer-workflow/supabase.ts +++ b/packages/workflows/src/transfer-workflow/supabase.ts @@ -9,6 +9,7 @@ export async function isTransferIndexed(hash: `0x${string}`) { .eq('tx_hash', hexToBytea(hash)) .single() + log.info('isTransferIndexed', { count, error, status, statusText }) if (error) { if (error.code === 'PGRST116') { log.info('isTransferIndexedActivity', { error }) diff --git a/packages/workflows/src/transfer-workflow/wagmi.ts b/packages/workflows/src/transfer-workflow/wagmi.ts index c009fde4b..eefab60ac 100644 --- a/packages/workflows/src/transfer-workflow/wagmi.ts +++ b/packages/workflows/src/transfer-workflow/wagmi.ts @@ -1,13 +1,7 @@ import { log, ApplicationFailure } from '@temporalio/activity' import type { UserOperation } from 'permissionless' -import { - baseMainnetBundlerClient, - baseMainnetClient, - sendAccountAbi, - tokenPaymasterAddress, - entryPointAddress, -} from '@my/wagmi' -import { encodeFunctionData, erc20Abi, isAddress, type Hex } from 'viem' +import { baseMainnetBundlerClient, baseMainnetClient, entryPointAddress } from '@my/wagmi' +import type { Hex } from 'viem' /** * default user op with preset gas values that work will probably need to move this to the database. @@ -46,10 +40,20 @@ export async function simulateUserOperation(userOp: UserOperation<'v0.7'>) { } export async function sendUserOperation(userOp: UserOperation<'v0.7'>) { - const hash = await baseMainnetBundlerClient.sendUserOperation({ - userOperation: userOp, - }) - return hash + log.info('Sending UserOperation', { userOp: JSON.stringify(userOp, null, 2) }) + try { + const hash = await baseMainnetBundlerClient.sendUserOperation({ + userOperation: userOp, + }) + log.info('UserOperation sent successfully', { hash }) + return hash + } catch (error) { + log.error('Error in sendUserOperation', { + error: error instanceof Error ? error.message : String(error), + userOp: JSON.stringify(userOp, null, 2), + }) + throw error + } } export async function waitForTransactionReceipt(hash: `0x${string}`) { diff --git a/packages/workflows/src/transfer-workflow/workflow.ts b/packages/workflows/src/transfer-workflow/workflow.ts index 340a210d6..e1b423434 100644 --- a/packages/workflows/src/transfer-workflow/workflow.ts +++ b/packages/workflows/src/transfer-workflow/workflow.ts @@ -1,6 +1,10 @@ import { proxyActivities, ApplicationFailure, defineQuery, setHandler } from '@temporalio/workflow' import type { createTransferActivities } from './activities' import type { UserOperation, GetUserOperationReceiptReturnType } from 'permissionless' +import debug from 'debug' +import superjson from 'superjson' + +const log = debug('workflows:transfer') const { simulateUserOpActivity, @@ -21,8 +25,8 @@ type indexing = { } type confirmed = { status: 'confirmed' - data: { receipt: GetUserOperationReceiptReturnType; userOp: UserOperation<'v0.7'> } -} + receipt: GetUserOperationReceiptReturnType | boolean +} & BaseState export type transferState = simulating | sending | waiting | indexing | confirmed @@ -30,17 +34,23 @@ export const getTransferStateQuery = defineQuery('getTransferStat export async function TransferWorkflow(userOp: UserOperation<'v0.7'>) { setHandler(getTransferStateQuery, () => ({ status: 'simulating', data: { userOp } })) + log('SendTransferWorkflow started with userOp:', JSON.stringify(parsedUserOp, null, 2)) await simulateUserOpActivity(userOp) + log('Simulation completed') setHandler(getTransferStateQuery, () => ({ status: 'sending', data: { userOp } })) + log('Sending UserOperation') const hash = await sendUserOpActivity(userOp) if (!hash) throw ApplicationFailure.nonRetryable('No hash returned from sendUserOperation') + log('UserOperation sent, hash:', hash) setHandler(getTransferStateQuery, () => ({ status: 'waiting', data: { userOp, hash } })) const receipt = await waitForTransactionReceiptActivity(hash) if (!receipt) throw ApplicationFailure.nonRetryable('No receipt returned from waitForTransactionReceipt') + log('Receipt received:', superjson.stringify(receipt)) setHandler(getTransferStateQuery, () => ({ status: 'indexing', data: { userOp, receipt } })) const transfer = await isTransferIndexedActivity(receipt.userOpHash) if (!transfer) throw ApplicationFailure.retryable('Transfer not yet indexed in db') + log('Transfer indexed:', superjson.stringify(transfer)) setHandler(getTransferStateQuery, () => ({ status: 'confirmed', data: { userOp, receipt } })) return transfer } diff --git a/packages/workflows/tsconfig.json b/packages/workflows/tsconfig.json index a4708d010..16e167628 100644 --- a/packages/workflows/tsconfig.json +++ b/packages/workflows/tsconfig.json @@ -9,7 +9,8 @@ "app/*": ["../app/*"], "@my/wagmi": ["../wagmi/src"], "@my/wagmi/*": ["../wagmi/src/*"], - "@my/api/*": ["../api/src/*"] + "@my/api/*": ["../api/src/*"], + "@my/workflows": ["./packages/workflows/src/all-workflows.ts"] } }, "include": [ diff --git a/yarn.lock b/yarn.lock index 8fd671e7d..91593cff2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31156,7 +31156,8 @@ __metadata: eslint: "npm:^8.46.0" lefthook: "npm:^1.5.5" node-gyp: "npm:^9.3.1" - turbo: "npm:^2.0.3" + snaplet: "npm:^0.42.1" + turbo: "npm:^2.1.2" typescript: "npm:^5.5.3" zx: "npm:^8.1.2" languageName: unknown @@ -33568,58 +33569,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.0.4": - version: 2.0.4 - resolution: "turbo-darwin-64@npm:2.0.4" +"turbo-darwin-64@npm:2.1.3": + version: 2.1.3 + resolution: "turbo-darwin-64@npm:2.1.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:2.0.4": - version: 2.0.4 - resolution: "turbo-darwin-arm64@npm:2.0.4" +"turbo-darwin-arm64@npm:2.1.3": + version: 2.1.3 + resolution: "turbo-darwin-arm64@npm:2.1.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:2.0.4": - version: 2.0.4 - resolution: "turbo-linux-64@npm:2.0.4" +"turbo-linux-64@npm:2.1.3": + version: 2.1.3 + resolution: "turbo-linux-64@npm:2.1.3" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:2.0.4": - version: 2.0.4 - resolution: "turbo-linux-arm64@npm:2.0.4" +"turbo-linux-arm64@npm:2.1.3": + version: 2.1.3 + resolution: "turbo-linux-arm64@npm:2.1.3" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:2.0.4": - version: 2.0.4 - resolution: "turbo-windows-64@npm:2.0.4" +"turbo-windows-64@npm:2.1.3": + version: 2.1.3 + resolution: "turbo-windows-64@npm:2.1.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:2.0.4": - version: 2.0.4 - resolution: "turbo-windows-arm64@npm:2.0.4" +"turbo-windows-arm64@npm:2.1.3": + version: 2.1.3 + resolution: "turbo-windows-arm64@npm:2.1.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:^2.0.3": - version: 2.0.4 - resolution: "turbo@npm:2.0.4" - dependencies: - turbo-darwin-64: "npm:2.0.4" - turbo-darwin-arm64: "npm:2.0.4" - turbo-linux-64: "npm:2.0.4" - turbo-linux-arm64: "npm:2.0.4" - turbo-windows-64: "npm:2.0.4" - turbo-windows-arm64: "npm:2.0.4" +"turbo@npm:^2.1.2": + version: 2.1.3 + resolution: "turbo@npm:2.1.3" + dependencies: + turbo-darwin-64: "npm:2.1.3" + turbo-darwin-arm64: "npm:2.1.3" + turbo-linux-64: "npm:2.1.3" + turbo-linux-arm64: "npm:2.1.3" + turbo-windows-64: "npm:2.1.3" + turbo-windows-arm64: "npm:2.1.3" dependenciesMeta: turbo-darwin-64: optional: true @@ -33635,7 +33636,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/689b54d58c04ef04c81ade5f91edbab0805ec270d55f8d878f6958024e216ec06a82bea3246e117d631f408e3c2b5dba3e5d58df0fba80470c231cfd5d698793 + checksum: 10/b8e90a38f47dc5c07e5f1c0bd708f9dc6b00b744847a45c06e5de5a5379a32bb155e8ad994eb03e60f697afc87f0815dd02fc680e22c0fad83d65c0a1fb6fc96 languageName: node linkType: hard From 7850eb26f8dd70f361aea556b88158521f9fb399 Mon Sep 17 00:00:00 2001 From: Beezy Date: Tue, 1 Oct 2024 03:13:39 +0000 Subject: [PATCH 10/15] remove debug env --- apps/next/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/next/package.json b/apps/next/package.json index f8cd50bd3..65c27f771 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -11,7 +11,7 @@ "serve": "NODE_ENV=production yarn with-env next start --port 8151", "lint": "next lint", "lint:fix": "next lint --fix", - "with-env": "TAMAGUI_TARGET=web dotenv -e ../../.env.localtemp -c --" + "with-env": "TAMAGUI_TARGET=web dotenv -e ../../.env -c --" }, "dependencies": { "@my/api": "workspace:*", From 0f2db411ea7c9521f46a8705589e49f9177e9a09 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 11 Sep 2024 16:04:47 -0700 Subject: [PATCH 11/15] Show pending transfers from temporal in UI --- .gitignore | 2 + CONTRIBUTING.md | 6 +- apps/workers/package.json | 6 +- apps/workers/src/client.ts | 4 +- apps/workers/src/worker.ts | 25 +- apps/workers/tsconfig.json | 10 +- biome.json | 4 +- environment.d.ts | 7 + packages/api/package.json | 2 +- packages/api/src/routers/transfer.ts | 6 +- packages/api/tsconfig.json | 15 +- .../app/features/home/TokenActivityRow.tsx | 94 +- .../app/features/home/TokenDetailsHistory.tsx | 181 ++-- .../__snapshots__/TokenDetails.test.tsx.snap | 881 +++++++++++++++++ packages/app/features/home/screen.tsx | 2 +- .../utils/__mocks__/useTokenActivityFeed.ts | 21 +- .../home/utils/usePendingTransfers.ts | 22 + .../home/utils/useTokenActivityFeed.ts | 65 +- packages/app/features/send/confirm/screen.tsx | 61 +- packages/app/package.json | 2 +- packages/app/tsconfig.json | 10 +- packages/app/utils/decodeTransferUserOp.ts | 23 + packages/app/utils/signUserOp.ts | 2 +- packages/playwright/tsconfig.json | 10 +- packages/snaplet/.snaplet/dataModel.json | 856 +++-------------- packages/temporal/.gitignore | 6 + packages/temporal/build/payload-converter.cjs | 882 ++++++++++++++++++ packages/temporal/package.json | 6 +- packages/temporal/src/client.ts | 6 +- packages/temporal/tsconfig.json | 13 + packages/workflows/.gitignore | 2 + packages/workflows/README.md | 16 + packages/workflows/package.json | 13 +- .../src/distribution-workflow/activities.ts | 546 +++++------ .../src/scripts/build-workflow-bundle.ts | 21 + .../src/transfer-workflow/activities.ts | 100 +- .../src/transfer-workflow/supabase.ts | 11 +- .../workflows/src/transfer-workflow/wagmi.ts | 8 +- .../src/transfer-workflow/workflow.ts | 32 +- packages/workflows/src/utils/bootstrap.ts | 30 + packages/workflows/src/utils/index.ts | 1 + packages/workflows/tsconfig.json | 11 +- tilt/apps.Tiltfile | 1 + tilt/deps.Tiltfile | 10 + tsconfig.base.json | 1 + tsconfig.json | 2 + yarn.lock | 332 ++++++- 47 files changed, 3087 insertions(+), 1280 deletions(-) create mode 100644 packages/app/features/home/utils/usePendingTransfers.ts create mode 100644 packages/app/utils/decodeTransferUserOp.ts create mode 100644 packages/temporal/.gitignore create mode 100644 packages/temporal/build/payload-converter.cjs create mode 100644 packages/temporal/tsconfig.json create mode 100644 packages/workflows/src/scripts/build-workflow-bundle.ts create mode 100644 packages/workflows/src/utils/bootstrap.ts create mode 100644 packages/workflows/src/utils/index.ts diff --git a/.gitignore b/.gitignore index d215fe6d4..710033e73 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ Brewfile.lock.json # asdf .tool-versions + +var/** \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91a65df6e..127c4f2d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,9 +36,13 @@ Here is a quick peek at the send stack. Quickly jump to any of the submodules by │   ├── daimo-expo-passkeys │   ├── eslint-config-customs │   ├── playwright +| ├── shovel +│   ├── snaplet +| ├── temporal │   ├── ui │   ├── wagmi -│   └── webauthn-authenticator +| ├── webauthn-authenticator +│   └── workflows └── supabase diff --git a/apps/workers/package.json b/apps/workers/package.json index 069f7b071..fce90a4b1 100644 --- a/apps/workers/package.json +++ b/apps/workers/package.json @@ -8,11 +8,13 @@ ], "scripts": { "lint": "tsc", - "start": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/worker.ts", - "workflow": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/client.ts" + "start": "yarn with-env node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/worker.ts", + "workflow": "yarn with-env node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/client.ts", + "with-env": "dotenv -e ../../.env -c --" }, "devDependencies": { "@types/bun": "^1.1.6", + "dotenv-cli": "^7.3.0", "ts-node": "^10.9.2", "typescript": "^5.5.3" }, diff --git a/apps/workers/src/client.ts b/apps/workers/src/client.ts index 6fe55a7ab..7b7be2fed 100644 --- a/apps/workers/src/client.ts +++ b/apps/workers/src/client.ts @@ -1,5 +1,5 @@ import { Connection, Client } from '@temporalio/client' -import { TransferWorkflow } from '@my/workflows/workflows' +import { TransferWorkflow } from '@my/workflows/all-workflows' import type { UserOperation } from 'permissionless' // async function runDistributionWorkflow() { @@ -31,7 +31,7 @@ export async function runTransferWorkflow(userOp: UserOperation<'v0.7'>) { connection, }) - const handle = await client.workflow.start(SendTransferWorkflow, { + const handle = await client.workflow.start(TransferWorkflow, { taskQueue: 'monorepo', workflowId: `transfers-workflow-${userOp.sender}-${userOp.nonce.toString()}`, // TODO: remember to replace this with a meaningful business ID args: [userOp], diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index 151de0bbc..6a9fa3351 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -1,16 +1,21 @@ -import { Worker, NativeConnection } from '@temporalio/worker' -import { - createTransferActivities, - createDistributionActivities, -} from '@my/workflows/all-activities' +import { Worker, NativeConnection, bundleWorkflowCode } from '@temporalio/worker' +import { createTransferActivities } from '@my/workflows/all-activities' import fs from 'node:fs/promises' import { createRequire } from 'node:module' -import { dataConverter } from '@my/temporal/payload-converter' const require = createRequire(import.meta.url) const { NODE_ENV = 'development' } = process.env const isDeployed = ['production', 'staging'].includes(NODE_ENV) +const workflowOption = () => + isDeployed + ? { + workflowBundle: { + codePath: require.resolve('@my/workflows/workflow-bundle'), + }, + } + : { workflowsPath: require.resolve('@my/workflows/all-workflows') } + async function run() { const connection = isDeployed ? await NativeConnection.connect({ @@ -32,12 +37,14 @@ async function run() { const worker = await Worker.create({ connection, - dataConverter: dataConverter, - workflowsPath: require.resolve('@my/workflows'), + dataConverter: { + payloadConverterPath: require.resolve('@my/temporal/payload-converter'), + }, + ...workflowOption(), activities: { ...createTransferActivities(process.env), }, - namespace: 'default', + namespace: process.env.TEMPORAL_NAMESPACE ?? 'default', taskQueue: 'monorepo', bundlerOptions: { ignoreModules: ['@supabase/supabase-js'], diff --git a/apps/workers/tsconfig.json b/apps/workers/tsconfig.json index 6ad8297a0..ad6707fd2 100644 --- a/apps/workers/tsconfig.json +++ b/apps/workers/tsconfig.json @@ -12,16 +12,18 @@ "composite": true, "baseUrl": ".", "paths": { - "@my/workflows": ["../../packages/workflows/src/*"], - "@my/workflows/*": ["../../packages/workflows/src/*"] + "@my/workflows": ["../../packages/workflows/src/all-workflows.ts"], + "@my/workflows/*": ["../../packages/workflows/src/*"], + "@my/temporal": ["../../packages/temporal/src"], + "@my/temporal/*": ["../../packages/temporal/src/*"] } }, "references": [], "include": [ "./src", "../../packages/workflows/src", + "../../packages/temporal/src", "../../globals.d.ts", - "../../environment.d.ts", - "../../packages/temporal/src" + "../../environment.d.ts" ] } diff --git a/biome.json b/biome.json index 1cd9d848e..cc7f7359d 100644 --- a/biome.json +++ b/biome.json @@ -23,7 +23,9 @@ "./packages/app/components/img/**", "packages/shovel/etc/config.json", "./supabase/.temp/**", - "./packages/contracts/var/*.json" + "./packages/contracts/var/*.json", + "**/tsconfig.json", + "**/*.tsconfig.json" ] }, "organizeImports": { diff --git a/environment.d.ts b/environment.d.ts index 1b9d5b81e..065d107a6 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -16,6 +16,13 @@ declare global { NEXT_PUBLIC_SUPABASE_PROJECT_ID: string NEXT_PUBLIC_SUPABASE_GRAPHQL_URL: string NEXT_PUBLIC_MAINNET_RPC_URL: string + /** + * The URL of the ERC 4337 Account Abstraction Bundler RPC endpoint + */ + BUNDLER_RPC_URL: string + /** + * The URL of the ERC 4337 Account Abstraction Bundler RPC endpoint + */ NEXT_PUBLIC_BASE_RPC_URL: string NEXT_PUBLIC_BUNDLER_RPC_URL: string SUPABASE_DB_URL: string diff --git a/packages/api/package.json b/packages/api/package.json index 60de2ccd2..09ec30b99 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -20,7 +20,7 @@ "ms": "^2.1.3", "p-queue": "^8.0.1", "permissionless": "^0.1.14", - "superjson": "^1.13.1", + "superjson": "^2.2.1", "viem": "^2.18.2", "zod": "^3.23.8" } diff --git a/packages/api/src/routers/transfer.ts b/packages/api/src/routers/transfer.ts index be70743cc..5b3b79230 100644 --- a/packages/api/src/routers/transfer.ts +++ b/packages/api/src/routers/transfer.ts @@ -4,8 +4,7 @@ import { z } from 'zod' import { createTRPCRouter, protectedProcedure } from '../trpc' import { getTemporalClient } from '@my/temporal/client' import type { UserOperation } from 'permissionless' - -import { TransferWorkflow, type transferState } from '@my/workflows' +import { TransferWorkflow, type transferState } from '@my/workflows/all-workflows' import type { allCoins } from 'app/data/coins' const log = debug('api:transfer') @@ -24,7 +23,7 @@ export const transferRouter = createTRPCRouter({ const client = await getTemporalClient() const handle = await client.workflow.start(TransferWorkflow, { taskQueue: 'monorepo', - workflowId: `send-transfer-workflow-${token}-${sender}-${nonce}`, + workflowId: `transfer-workflow-${token}-${sender}-${nonce}`, args: [userOp], }) log(`Workflow Created: ${handle.workflowId}`) @@ -65,6 +64,7 @@ export const transferRouter = createTRPCRouter({ }) for await (const workflow of workflows) { const handle = client.workflow.getHandle(workflow.workflowId) + console.log('handle: ', handle) const state = await handle.query('getTransferState') states.push(state) diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index d2e8cdc17..6b58d1e2d 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,6 +1,14 @@ { "extends": "../../tsconfig.base", - "include": ["src", "../app", "../wagmi/src", "../ui/src", "../../supabase"], + "include": [ + "src", + "../app", + "../wagmi/src", + "../ui/src", + "../../supabase", + "../workflows/src", + "../temporal/src" + ], "compilerOptions": { "noEmit": true, "composite": true, @@ -10,7 +18,10 @@ "@my/wagmi": ["../wagmi/src"], "@my/wagmi/*": ["../wagmi/src/*"], "@my/supabase/*": ["../../supabase/*"], - "@my/workflows": ["./packages/workflows/src/all-workflows.ts"] + "@my/workflows": ["../workflows/src/all-workflows.ts"], + "@my/workflows/*": ["../workflows/src/*"], + "@my/temporal": ["../temporal/src"], + "@my/temporal/*": ["../temporal/src/*"] } }, "references": [] diff --git a/packages/app/features/home/TokenActivityRow.tsx b/packages/app/features/home/TokenActivityRow.tsx index 2be625064..bf229715c 100644 --- a/packages/app/features/home/TokenActivityRow.tsx +++ b/packages/app/features/home/TokenActivityRow.tsx @@ -1,4 +1,4 @@ -import { Paragraph, Text, XStack, YStack } from '@my/ui' +import { Avatar, LinkableAvatar, Spinner, Paragraph, Text, XStack, YStack, Stack } from '@my/ui' import { amountFromActivity, eventNameFromActivity, subtextFromActivity } from 'app/utils/activity' import { isSendAccountReceiveEvent, @@ -8,9 +8,14 @@ import { import { ActivityAvatar } from '../activity/ActivityAvatar' import { CommentsTime } from 'app/utils/dateHelper' import { Link } from 'solito/link' - +import type { CoinWithBalance } from 'app/data/coins' import { useUser } from 'app/utils/useUser' import { useHoverStyles } from 'app/utils/useHoverStyles' +import type { transferState } from '@my/workflows' +import { sendAccountAbi, erc20Abi } from '@my/wagmi' +import { decodeFunctionData, formatUnits } from 'viem' +import { useProfileLookup } from 'app/utils/useProfileLookup' +import formatAmount from 'app/utils/formatAmount' export function TokenActivityRow({ activity, @@ -109,3 +114,88 @@ export function TokenActivityRow({ ) } + +export function PendingTransferActivityRow({ + coin, + state, +}: { coin: CoinWithBalance; state: transferState }) { + const { userOp } = state + const { args } = decodeFunctionData({ abi: sendAccountAbi, data: userOp.callData }) + + const decodedTokenTransfer = + args?.[0]?.[0].data !== '0x' + ? decodeFunctionData({ abi: erc20Abi, data: args?.[0]?.[0].data }) + : undefined + + const amount = decodedTokenTransfer + ? formatUnits(decodedTokenTransfer.args[1] as bigint, coin.decimals) + : formatAmount(formatUnits(args?.[0]?.[0].value, 18), 5, 5) + + const to = decodedTokenTransfer ? decodedTokenTransfer.args[0] : args?.[0]?.[0].dest + + const { data: profile } = useProfileLookup('address', to) + + return ( + + + + + + + + + + + + + + Sending... + + + {`${amount} ${coin.symbol}`} + + + + + {profile?.name ?? profile?.tag ?? profile?.sendid} + + + + + + + ) +} diff --git a/packages/app/features/home/TokenDetailsHistory.tsx b/packages/app/features/home/TokenDetailsHistory.tsx index 9f7254d3e..e776b9e9e 100644 --- a/packages/app/features/home/TokenDetailsHistory.tsx +++ b/packages/app/features/home/TokenDetailsHistory.tsx @@ -1,93 +1,134 @@ -import { Button, Card, Label, Paragraph, Spinner } from '@my/ui' +import { Button, Card, Label, Paragraph, Spinner, YStack } from '@my/ui' import type { CoinWithBalance } from 'app/data/coins' -import { hexToBytea } from 'app/utils/hexToBytea' -import { Fragment } from 'react' +import { Fragment, useEffect, useState } from 'react' import { useTokenActivityFeed } from './utils/useTokenActivityFeed' -import { AnimateEnter } from './TokenDetails' -import { TokenActivityRow } from './TokenActivityRow' +import { RowLabel, AnimateEnter } from './TokenDetails' +import { PendingTransferActivityRow, TokenActivityRow } from './TokenActivityRow' +import { useSendAccount } from 'app/utils/send-accounts' +import { zeroAddress } from 'viem' export const TokenDetailsHistory = ({ coin }: { coin: CoinWithBalance }) => { - const result = useTokenActivityFeed({ + const { data: sendAccount } = useSendAccount() + const [hasPendingTransfers, setHasPendingTransfers] = useState(true) + const { pendingTransfers, activityFeed } = useTokenActivityFeed({ + address: sendAccount?.address ?? zeroAddress, + token: coin.token, pageSize: 10, - address: coin.token === 'eth' ? undefined : hexToBytea(coin.token), + enabled: + (hasPendingTransfers === undefined || hasPendingTransfers) && + sendAccount?.address !== undefined, }) + console.log('pendingTransfers: ', pendingTransfers) + + const { data: pendingTransfersData, isError: pendingTransfersError } = pendingTransfers + const { - data, + data: activityFeedData, isLoading: isLoadingActivities, error: activitiesError, isFetching: isFetchingActivities, isFetchingNextPage: isFetchingNextPageActivities, fetchNextPage, hasNextPage, - } = result - const { pages } = data ?? {} - if (isLoadingActivities) return + } = activityFeed + + const { pages } = activityFeedData ?? {} + + // Check if there are any pending transfers in the temporal db. If not set hasPendingTransfers to false to control refetches + useEffect(() => { + if (Array.isArray(pendingTransfersData)) { + setHasPendingTransfers(pendingTransfersData?.length > 0) + } else if (pendingTransfersError) { + setHasPendingTransfers(false) + } else { + setHasPendingTransfers(undefined) + } + }, [pendingTransfersData, pendingTransfersError]) + return ( <> - {(() => { - switch (true) { - case activitiesError !== null: - return ( - - {activitiesError?.message.split('.').at(0) ?? `${activitiesError}`} - - ) - case !pages || !pages[0]?.length: - return null - default: { - let lastDate: string | undefined - return ( - - {pages?.map((activities) => { - return activities.map((activity) => { - const date = activity.created_at.toLocaleDateString() - const isNewDate = !lastDate || date !== lastDate - if (isNewDate) { - lastDate = date - } - return ( - - - - - - ) - }) - })} - - {!isLoadingActivities && (isFetchingNextPageActivities || hasNextPage) ? ( - <> - {isFetchingNextPageActivities && ( - - )} - {hasNextPage && ( - - )} - - ) : null} - - - ) + + + + + ) + }) + })} + + {!isLoadingActivities && (isFetchingNextPageActivities || hasNextPage) ? ( + <> + {isFetchingNextPageActivities && ( + + )} + {hasNextPage && ( + + )} + + ) : null} + + + ) + } } - } - })()} + })()} + ) } diff --git a/packages/app/features/home/__snapshots__/TokenDetails.test.tsx.snap b/packages/app/features/home/__snapshots__/TokenDetails.test.tsx.snap index 943c179b1..4793248aa 100644 --- a/packages/app/features/home/__snapshots__/TokenDetails.test.tsx.snap +++ b/packages/app/features/home/__snapshots__/TokenDetails.test.tsx.snap @@ -284,6 +284,887 @@ exports[`TokenDetails 1`] = ` + + + + + + + + + + + + + + + + + + + + Withdraw + + + 10 USDC + + + + + 0x93F...761a + + + 7 mon ago + + + + + + + + + + + + + + + + + + + + + + + + + Deposit + + + 20 USDC + + + + + 0xa71...0000 + + + 7 mon ago + + + + + + + + + + + + + + + + + + + + + + + + + Received + + + 30 USDC + + + + + + /alice + + + + 7 mon ago + + + + + + + + "backgroundColor": "#111f22", "borderBottomLeftRadius": 16, "borderBottomRightRadius": 16, diff --git a/packages/app/features/home/screen.tsx b/packages/app/features/home/screen.tsx index dcff65e7a..47676f321 100644 --- a/packages/app/features/home/screen.tsx +++ b/packages/app/features/home/screen.tsx @@ -140,7 +140,7 @@ export function HomeScreen() { No send account found ) - case search !== undefined: + case search !== undefined: //@todo remove this return default: return ( diff --git a/packages/app/features/home/utils/__mocks__/useTokenActivityFeed.ts b/packages/app/features/home/utils/__mocks__/useTokenActivityFeed.ts index 59f2b667f..a0d33a661 100644 --- a/packages/app/features/home/utils/__mocks__/useTokenActivityFeed.ts +++ b/packages/app/features/home/utils/__mocks__/useTokenActivityFeed.ts @@ -1,5 +1,6 @@ import { SendAccountTransfersEventSchema } from 'app/utils/zod/activity' import { mockUsdcTransfers } from './mock-usdc-transfers' +import { hexToBytea } from 'app/utils/hexToBytea' const tokenTransfersByLogAddr = { '\\x833589fcd6edb6e08f4c7c32d4f71b54bda02913': mockUsdcTransfers.map((t) => @@ -7,15 +8,23 @@ const tokenTransfersByLogAddr = { ), } -const mockUseTokenActivityFeed = jest.fn(({ address }) => { - const pages = tokenTransfersByLogAddr[address] +const mockUseTokenActivityFeed = jest.fn(({ token }) => { + const logAddress = hexToBytea(token) + const pages = tokenTransfersByLogAddr[logAddress] if (!pages) throw new Error('No pages found') return { - data: { - pages: [tokenTransfersByLogAddr[address]], + pendingTransfers: { + data: [], //@todo maybe writes some mock data for temporal? + isLoading: false, + error: null, + }, + activityFeed: { + data: { + pages: [tokenTransfersByLogAddr[logAddress]], + }, + isLoading: false, + error: null, }, - isLoading: false, - error: null, } }) export const useTokenActivityFeed = mockUseTokenActivityFeed diff --git a/packages/app/features/home/utils/usePendingTransfers.ts b/packages/app/features/home/utils/usePendingTransfers.ts new file mode 100644 index 000000000..09a21d4c7 --- /dev/null +++ b/packages/app/features/home/utils/usePendingTransfers.ts @@ -0,0 +1,22 @@ +import type { Address } from 'viem' +import type { allCoins } from 'app/data/coins' +import { api } from 'app/utils/api' + +/** + * Fetch Pending transfers by token and send account address + */ +export function usePendingTransfers(params: { + address: Address + token: allCoins[number]['token'] + refetchInterval?: number + enabled?: boolean +}) { + const { address, token, refetchInterval, enabled } = params + return api.transfer.getPending.useQuery( + { token, sender: address }, + { + refetchInterval, + enabled, + } + ) +} diff --git a/packages/app/features/home/utils/useTokenActivityFeed.ts b/packages/app/features/home/utils/useTokenActivityFeed.ts index 87eeb10a7..33b7d385c 100644 --- a/packages/app/features/home/utils/useTokenActivityFeed.ts +++ b/packages/app/features/home/utils/useTokenActivityFeed.ts @@ -1,20 +1,18 @@ -import type { PgBytea } from '@my/supabase/database.types' import { sendTokenV0LockboxAddress, tokenPaymasterAddress } from '@my/wagmi' -import type { PostgrestError } from '@supabase/postgrest-js' -import { - useInfiniteQuery, - type InfiniteData, - type UseInfiniteQueryResult, -} from '@tanstack/react-query' +import { useInfiniteQuery } from '@tanstack/react-query' import { pgAddrCondValues } from 'app/utils/pgAddrCondValues' import { squish } from 'app/utils/strings' import { useSupabase } from 'app/utils/supabase/useSupabase' import { throwIf } from 'app/utils/throwIf' import { EventArraySchema, Events, type Activity } from 'app/utils/zod/activity' -import type { ZodError } from 'zod' +import { usePendingTransfers } from './usePendingTransfers' +import type { Address } from 'viem' +import type { allCoins } from 'app/data/coins' /** - * Infinite query to fetch ERC-20 token activity feed. + * Returns two hooks + * 1. useTokenActivityFeed - Infinite query to fetch ERC-20 token activity feed. + * 2. usePendingTransfers - Returns a list from temporal of pending transfers for the given address and token * * @note does not support ETH transfers. Need to add another shovel integration to handle ETH receives, and another one for ETH sends * @@ -22,11 +20,12 @@ import type { ZodError } from 'zod' */ export function useTokenActivityFeed(params: { pageSize?: number - address?: PgBytea + address: Address + token: allCoins[number]['token'] refetchInterval?: number enabled?: boolean -}): UseInfiniteQueryResult, PostgrestError | ZodError> { - const { pageSize = 10, address, refetchInterval = 30_000, enabled = true } = params +}) { + const { pageSize = 10, token, address, refetchInterval = 30_000, enabled = true } = params const supabase = useSupabase() async function fetchTokenActivityFeed({ pageParam }: { pageParam: number }): Promise { @@ -66,21 +65,29 @@ export function useTokenActivityFeed(params: { return EventArraySchema.parse(data) } - return useInfiniteQuery({ - queryKey: ['token_activity_feed', address], - initialPageParam: 0, - getNextPageParam: (lastPage, _allPages, lastPageParam) => { - if (lastPage !== null && lastPage.length < pageSize) return undefined - return lastPageParam + 1 - }, - getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => { - if (firstPageParam <= 1) { - return undefined - } - return firstPageParam - 1 - }, - queryFn: fetchTokenActivityFeed, - refetchInterval, - enabled, - }) + return { + pendingTransfers: usePendingTransfers({ + address: address, + token, + refetchInterval, + enabled, + }), + activityFeed: useInfiniteQuery({ + queryKey: ['token_activity_feed', token], + initialPageParam: 0, + getNextPageParam: (lastPage, _allPages, lastPageParam) => { + if (lastPage !== null && lastPage.length < pageSize) return undefined + return lastPageParam + 1 + }, + getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => { + if (firstPageParam <= 1) { + return undefined + } + return firstPageParam - 1 + }, + queryFn: fetchTokenActivityFeed, + refetchInterval, + enabled, + }), + } } diff --git a/packages/app/features/send/confirm/screen.tsx b/packages/app/features/send/confirm/screen.tsx index 77ef7f225..75ea78ab5 100644 --- a/packages/app/features/send/confirm/screen.tsx +++ b/packages/app/features/send/confirm/screen.tsx @@ -29,16 +29,16 @@ import { useGenerateTransferUserOp } from 'app/utils/useUserOpTransferMutation' import { useAccountNonce } from 'app/utils/userop' import { useEffect, useState } from 'react' import { useRouter } from 'solito/router' -import { formatUnits, isAddress } from 'viem' +import { formatUnits, isAddress, zeroAddress } from 'viem' import { useEstimateFeesPerGas } from 'wagmi' import { useCoin } from 'app/provider/coins' import { useCoinFromSendTokenParam } from 'app/utils/useCoinFromTokenParam' import { allCoinsDict } from 'app/data/coins' import { api } from 'app/utils/api' -import { TRPCClientError } from '@trpc/client' import { getUserOperationHash } from 'permissionless' import { signUserOp } from 'app/utils/signUserOp' import { byteaToBase64 } from 'app/utils/byteaToBase64' +import { usePendingTransfers } from 'app/features/home/utils/usePendingTransfers' export function SendConfirmScreen() { const [queryParams] = useSendScreenParams() @@ -66,17 +66,27 @@ export function SendConfirmScreen() { } export function SendConfirm() { + const router = useRouter() const [queryParams] = useSendScreenParams() const { sendToken, recipient, idType, amount } = queryParams - const { - mutateAsync: transfer, - isPending: isTransferPending, - isError: isTransferError, - } = api.transfer.withUserOp.useMutation() - - const queryClient = useQueryClient() const { data: sendAccount, isLoading: isSendAccountLoading } = useSendAccount() const { coin: selectedCoin, tokensQuery, ethQuery } = useCoinFromSendTokenParam() + + const { mutateAsync: transfer } = api.transfer.withUserOp.useMutation() + const { data: pendingTransfers, isLoading: isPendingTransfersLoading } = usePendingTransfers({ + address: sendAccount?.address ?? zeroAddress, + token: sendToken, + }) + + const [workflowId, setWorkflowId] = useState() + + useEffect(() => { + if (workflowId) { + router.replace({ pathname: '/', query: { token: sendToken } }) + } + }, [workflowId, router, sendToken]) + + const queryClient = useQueryClient() const isUSDCSelected = selectedCoin?.label === 'USDC' const { coin: usdc } = useCoin('USDC') @@ -92,8 +102,6 @@ export function SendConfirm() { .filter((c) => !!c.webauthn_credentials) .map((c) => c.webauthn_credentials as NonNullable) ?? [] - const router = useRouter() - const { data: nonce, error: nonceError, @@ -107,7 +115,8 @@ export function SendConfirm() { to: profile?.address ?? recipient, token: sendToken === 'eth' ? undefined : sendToken, amount: BigInt(queryParams.amount ?? '0'), - nonce: nonce, + nonce: + nonce && pendingTransfers !== undefined ? nonce + BigInt(pendingTransfers.length) : nonce, }) const { @@ -193,18 +202,8 @@ export function SendConfirm() { }) userOp.signature = signature - const { data: workflowId, error } = await transfer({ - token: selectedCoin.token, - userOp, - }).catch((e) => { - console.error("Couldn't send the userOp", e) - if (e instanceof TRPCClientError) { - return { data: undefined, error: { message: e.message } } - } - return { data: undefined, error: { message: e.message } } - }) - console.log('workflowId', workflowId) - console.log('error', error) + const workflowId = await transfer({ userOp, token: sendToken }) + setWorkflowId(workflowId) if (selectedCoin?.token === 'eth') { await ethQuery.refetch() } else { @@ -217,8 +216,7 @@ export function SendConfirm() { } } - - if (isSendAccountLoading || nonceIsLoading || isProfileLoading) + if (nonceIsLoading || isProfileLoading || isSendAccountLoading || isPendingTransfersLoading) return return ( @@ -344,7 +342,7 @@ export function SendConfirm() { onPress={onSubmit} br={'$4'} disabledStyle={{ opacity: 0.7, cursor: 'not-allowed', pointerEvents: 'none' }} - disabled={!canSubmit || isTransferPending} + disabled={!canSubmit || !!workflowId} gap={4} py={'$5'} width={'100%'} @@ -357,15 +355,6 @@ export function SendConfirm() { ) - case isTransferPending && !isTransferError: - return ( - <> - - - - Sending... - - ) case !hasEnoughBalance: return Insufficient Balance case !hasEnoughGas: diff --git a/packages/app/package.json b/packages/app/package.json index 0444c9c30..1b9e4ed89 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -59,7 +59,7 @@ "react-native-svg": "15.2.0", "react-use-precision-timer": "^3.5.5", "solito": "^4.0.1", - "superjson": "^1.13.1", + "superjson": "^2.2.1", "viem": "^2.18.2", "wagmi": "^2.12.1", "zod": "^3.23.8" diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 8de9cca7a..ed88b0d15 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -21,7 +21,9 @@ "./.eslintrc.cjs", "../../supabase", "../../globals.d.ts", - "../../environment.d.ts" + "../../environment.d.ts", + "../temporal/src", + "../workflows/src" ], "exclude": ["coverage"], "compilerOptions": { @@ -39,7 +41,11 @@ "@my/contracts/out/*": ["../contracts/out/*"], "@my/supabase": ["../../supabase"], "@my/supabase/*": ["../../supabase/*"], - "@daimo/expo-passkeys/*": ["../daimo-expo-passkeys/src/*"] + "@daimo/expo-passkeys/*": ["../daimo-expo-passkeys/src/*"], + "@my/temporal": ["../temporal/src"], + "@my/temporal/*": ["../temporal/src/*"], + "@my/workflows": ["../workflows/src/all-workflows.ts"], + "@my/workflows/*": ["../workflows/src/*"] } }, "references": [] diff --git a/packages/app/utils/decodeTransferUserOp.ts b/packages/app/utils/decodeTransferUserOp.ts new file mode 100644 index 000000000..2856c654e --- /dev/null +++ b/packages/app/utils/decodeTransferUserOp.ts @@ -0,0 +1,23 @@ +import { decodeFunctionData } from 'viem' +import { sendAccountAbi, erc20Abi } from '@my/wagmi' +import type { UserOperation } from 'permissionless' +import type { coinsDict } from 'app/data/coins' + +export function decodeTransferUserOp({ userOp }: { userOp: UserOperation<'v0.7'> }) { + const { args } = decodeFunctionData({ abi: sendAccountAbi, data: userOp.callData }) + + const decodedTokenTransfer = + args?.[0]?.[0].data !== '0x' + ? decodeFunctionData({ abi: erc20Abi, data: args?.[0]?.[0].data }) + : undefined + + const amount = ( + decodedTokenTransfer ? decodedTokenTransfer.args[1] : args?.[0]?.[0].value + ) as bigint + + const to = ( + decodedTokenTransfer ? decodedTokenTransfer.args[0] : args?.[0]?.[0].dest + ) as `0x${string}` + const token = (decodedTokenTransfer ? args?.[0]?.[0].dest : 'eth') as keyof coinsDict + return { from: userOp.sender, to, token, amount } +} diff --git a/packages/app/utils/signUserOp.ts b/packages/app/utils/signUserOp.ts index 0db3b437c..9476d2a37 100644 --- a/packages/app/utils/signUserOp.ts +++ b/packages/app/utils/signUserOp.ts @@ -52,7 +52,7 @@ export async function signUserOpHash({ allowedCredentials?: { id: string; userHandle: string }[] }) { version = version ?? USEROP_VERSION - validUntil = validUntil ?? Math.floor((Date.now() + 1000 * 120) / 1000) // default 120 seconds (2 minutes) + validUntil = validUntil ?? Math.floor((Date.now() + 1000 * 35) / 1000) // default 35 seconds) allowedCredentials = allowedCredentials ?? [] assert(version === USEROP_VERSION, 'version must be 1') assert(typeof validUntil === 'number', 'validUntil must be a number') diff --git a/packages/playwright/tsconfig.json b/packages/playwright/tsconfig.json index 663daa184..ee2e72832 100644 --- a/packages/playwright/tsconfig.json +++ b/packages/playwright/tsconfig.json @@ -8,7 +8,9 @@ "../wagmi", "../contracts/out", "../webauthn-authenticator", - "../../supabase" + "../../supabase", + "../temporal/src", + "../workflows/src" ], "compilerOptions": { "noEmit": true, @@ -28,7 +30,11 @@ "@0xsend/webauthn-authenticator": ["../webauthn-authenticator/src"], "@0xsend/webauthn-authenticator/*": ["../webauthn-authenticator/src/*"], "@my/supabase": ["../../supabase"], - "@my/supabase/*": ["../../supabase/*"] + "@my/supabase/*": ["../../supabase/*"], + "@my/temporal": ["../temporal/src"], + "@my/temporal/*": ["../temporal/src/*"], + "@my/workflows": ["../workflows/src"], + "@my/workflows/*": ["../workflows/src/*"] } }, "references": [] diff --git a/packages/snaplet/.snaplet/dataModel.json b/packages/snaplet/.snaplet/dataModel.json index 319cd7ee0..9486a8ce2 100644 --- a/packages/snaplet/.snaplet/dataModel.json +++ b/packages/snaplet/.snaplet/dataModel.json @@ -637,34 +637,6 @@ "isGenerated": false, "sequence": false, "hasDefaultValue": false - }, - { - "name": "s3_multipart_uploads", - "type": "s3_multipart_uploads", - "isRequired": false, - "kind": "object", - "relationName": "s3_multipart_uploadsTobuckets", - "relationFromFields": [], - "relationToFields": [], - "isList": true, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - }, - { - "name": "s3_multipart_uploads_parts", - "type": "s3_multipart_uploads_parts", - "isRequired": false, - "kind": "object", - "relationName": "s3_multipart_uploads_partsTobuckets", - "relationFromFields": [], - "relationToFields": [], - "isList": true, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false } ], "uniqueConstraints": [ @@ -2005,20 +1977,6 @@ "isId": false, "maxLength": null }, - { - "id": "auth.flow_state.auth_code_issued_at", - "name": "auth_code_issued_at", - "columnName": "auth_code_issued_at", - "type": "timestamptz", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, { "name": "saml_relay_states", "type": "saml_relay_states", @@ -3708,146 +3666,6 @@ } ] }, - "one_time_tokens": { - "id": "auth.one_time_tokens", - "schemaName": "auth", - "tableName": "one_time_tokens", - "fields": [ - { - "id": "auth.one_time_tokens.id", - "name": "id", - "columnName": "id", - "type": "uuid", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": true, - "maxLength": null - }, - { - "id": "auth.one_time_tokens.user_id", - "name": "user_id", - "columnName": "user_id", - "type": "uuid", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.one_time_tokens.token_type", - "name": "token_type", - "columnName": "token_type", - "type": "one_time_token_type", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.one_time_tokens.token_hash", - "name": "token_hash", - "columnName": "token_hash", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.one_time_tokens.relates_to", - "name": "relates_to", - "columnName": "relates_to", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.one_time_tokens.created_at", - "name": "created_at", - "columnName": "created_at", - "type": "timestamp", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": true, - "isId": false, - "maxLength": null - }, - { - "id": "auth.one_time_tokens.updated_at", - "name": "updated_at", - "columnName": "updated_at", - "type": "timestamp", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": true, - "isId": false, - "maxLength": null - }, - { - "name": "users", - "type": "users", - "isRequired": true, - "kind": "object", - "relationName": "one_time_tokensTousers", - "relationFromFields": [ - "user_id" - ], - "relationToFields": [ - "id" - ], - "isList": false, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - } - ], - "uniqueConstraints": [ - { - "name": "one_time_tokens_pkey", - "fields": [ - "id" - ], - "nullNotDistinct": false - }, - { - "name": "one_time_tokens_user_id_token_type_key", - "fields": [ - "token_type", - "user_id" - ], - "nullNotDistinct": false - } - ] - }, "profiles": { "id": "public.profiles", "schemaName": "public", @@ -4214,371 +4032,7 @@ "name": "tag", "columnName": "tag", "type": "citext", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "public.referrals.id", - "name": "id", - "columnName": "id", - "type": "int4", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": { - "identifier": "\"public\".\"referrals_id_seq\"", - "increment": 1, - "start": 1 - }, - "hasDefaultValue": true, - "isId": true, - "maxLength": null - }, - { - "name": "profiles_referrals_referred_idToprofiles", - "type": "profiles", - "isRequired": true, - "kind": "object", - "relationName": "referrals_referred_idToprofiles", - "relationFromFields": [ - "referred_id" - ], - "relationToFields": [ - "id" - ], - "isList": false, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - }, - { - "name": "profiles_referrals_referrer_idToprofiles", - "type": "profiles", - "isRequired": true, - "kind": "object", - "relationName": "referrals_referrer_idToprofiles", - "relationFromFields": [ - "referrer_id" - ], - "relationToFields": [ - "id" - ], - "isList": false, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - }, - { - "name": "tags", - "type": "tags", - "isRequired": true, - "kind": "object", - "relationName": "referralsTotags", - "relationFromFields": [ - "tag" - ], - "relationToFields": [ - "name" - ], - "isList": false, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - } - ], - "uniqueConstraints": [ - { - "name": "referrals_pkey", - "fields": [ - "id" - ], - "nullNotDistinct": false - }, - { - "name": "unique_referred_id", - "fields": [ - "referred_id" - ], - "nullNotDistinct": false - } - ] - }, - "refresh_tokens": { - "id": "auth.refresh_tokens", - "schemaName": "auth", - "tableName": "refresh_tokens", - "fields": [ - { - "id": "auth.refresh_tokens.instance_id", - "name": "instance_id", - "columnName": "instance_id", - "type": "uuid", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.refresh_tokens.id", - "name": "id", - "columnName": "id", - "type": "int8", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": { - "identifier": "\"auth\".\"refresh_tokens_id_seq\"", - "increment": 1, - "start": 1 - }, - "hasDefaultValue": true, - "isId": true, - "maxLength": null - }, - { - "id": "auth.refresh_tokens.token", - "name": "token", - "columnName": "token", - "type": "varchar", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": 255 - }, - { - "id": "auth.refresh_tokens.user_id", - "name": "user_id", - "columnName": "user_id", - "type": "varchar", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": 255 - }, - { - "id": "auth.refresh_tokens.revoked", - "name": "revoked", - "columnName": "revoked", - "type": "bool", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.refresh_tokens.created_at", - "name": "created_at", - "columnName": "created_at", - "type": "timestamptz", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.refresh_tokens.updated_at", - "name": "updated_at", - "columnName": "updated_at", - "type": "timestamptz", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "auth.refresh_tokens.parent", - "name": "parent", - "columnName": "parent", - "type": "varchar", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": 255 - }, - { - "id": "auth.refresh_tokens.session_id", - "name": "session_id", - "columnName": "session_id", - "type": "uuid", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "name": "sessions", - "type": "sessions", - "isRequired": false, - "kind": "object", - "relationName": "refresh_tokensTosessions", - "relationFromFields": [ - "session_id" - ], - "relationToFields": [ - "id" - ], - "isList": false, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - } - ], - "uniqueConstraints": [ - { - "name": "refresh_tokens_pkey", - "fields": [ - "id" - ], - "nullNotDistinct": false - }, - { - "name": "refresh_tokens_token_unique", - "fields": [ - "token" - ], - "nullNotDistinct": false - } - ] - }, - "s3_multipart_uploads": { - "id": "storage.s3_multipart_uploads", - "schemaName": "storage", - "tableName": "s3_multipart_uploads", - "fields": [ - { - "id": "storage.s3_multipart_uploads.id", - "name": "id", - "columnName": "id", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": true, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads.in_progress_size", - "name": "in_progress_size", - "columnName": "in_progress_size", - "type": "int8", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": true, - "isId": false, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads.upload_signature", - "name": "upload_signature", - "columnName": "upload_signature", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads.bucket_id", - "name": "bucket_id", - "columnName": "bucket_id", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads.key", - "name": "key", - "columnName": "key", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads.version", - "name": "version", - "columnName": "version", - "type": "text", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads.owner_id", - "name": "owner_id", - "columnName": "owner_id", - "type": "text", - "isRequired": false, + "isRequired": true, "kind": "scalar", "isList": false, "isGenerated": false, @@ -4588,27 +4042,31 @@ "maxLength": null }, { - "id": "storage.s3_multipart_uploads.created_at", - "name": "created_at", - "columnName": "created_at", - "type": "timestamptz", + "id": "public.referrals.id", + "name": "id", + "columnName": "id", + "type": "int4", "isRequired": true, "kind": "scalar", "isList": false, "isGenerated": false, - "sequence": false, + "sequence": { + "identifier": "\"public\".\"referrals_id_seq\"", + "increment": 1, + "start": 1 + }, "hasDefaultValue": true, - "isId": false, + "isId": true, "maxLength": null }, { - "name": "buckets", - "type": "buckets", + "name": "profiles_referrals_referred_idToprofiles", + "type": "profiles", "isRequired": true, "kind": "object", - "relationName": "s3_multipart_uploadsTobuckets", + "relationName": "referrals_referred_idToprofiles", "relationFromFields": [ - "bucket_id" + "referred_id" ], "relationToFields": [ "id" @@ -4620,14 +4078,36 @@ "hasDefaultValue": false }, { - "name": "s3_multipart_uploads_parts", - "type": "s3_multipart_uploads_parts", - "isRequired": false, + "name": "profiles_referrals_referrer_idToprofiles", + "type": "profiles", + "isRequired": true, "kind": "object", - "relationName": "s3_multipart_uploads_partsTos3_multipart_uploads", - "relationFromFields": [], - "relationToFields": [], - "isList": true, + "relationName": "referrals_referrer_idToprofiles", + "relationFromFields": [ + "referrer_id" + ], + "relationToFields": [ + "id" + ], + "isList": false, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + }, + { + "name": "tags", + "type": "tags", + "isRequired": true, + "kind": "object", + "relationName": "referralsTotags", + "relationFromFields": [ + "tag" + ], + "relationToFields": [ + "name" + ], + "isList": false, "isId": false, "isGenerated": false, "sequence": false, @@ -4636,39 +4116,32 @@ ], "uniqueConstraints": [ { - "name": "s3_multipart_uploads_pkey", + "name": "referrals_pkey", "fields": [ "id" ], "nullNotDistinct": false + }, + { + "name": "unique_referred_id", + "fields": [ + "referred_id" + ], + "nullNotDistinct": false } ] }, - "s3_multipart_uploads_parts": { - "id": "storage.s3_multipart_uploads_parts", - "schemaName": "storage", - "tableName": "s3_multipart_uploads_parts", + "refresh_tokens": { + "id": "auth.refresh_tokens", + "schemaName": "auth", + "tableName": "refresh_tokens", "fields": [ { - "id": "storage.s3_multipart_uploads_parts.id", - "name": "id", - "columnName": "id", + "id": "auth.refresh_tokens.instance_id", + "name": "instance_id", + "columnName": "instance_id", "type": "uuid", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": true, - "isId": true, - "maxLength": null - }, - { - "id": "storage.s3_multipart_uploads_parts.upload_id", - "name": "upload_id", - "columnName": "upload_id", - "type": "text", - "isRequired": true, + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, @@ -4678,53 +4151,57 @@ "maxLength": null }, { - "id": "storage.s3_multipart_uploads_parts.size", - "name": "size", - "columnName": "size", + "id": "auth.refresh_tokens.id", + "name": "id", + "columnName": "id", "type": "int8", "isRequired": true, "kind": "scalar", "isList": false, "isGenerated": false, - "sequence": false, + "sequence": { + "identifier": "\"auth\".\"refresh_tokens_id_seq\"", + "increment": 1, + "start": 1 + }, "hasDefaultValue": true, - "isId": false, + "isId": true, "maxLength": null }, { - "id": "storage.s3_multipart_uploads_parts.part_number", - "name": "part_number", - "columnName": "part_number", - "type": "int4", - "isRequired": true, + "id": "auth.refresh_tokens.token", + "name": "token", + "columnName": "token", + "type": "varchar", + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, "sequence": false, "hasDefaultValue": false, "isId": false, - "maxLength": null + "maxLength": 255 }, { - "id": "storage.s3_multipart_uploads_parts.bucket_id", - "name": "bucket_id", - "columnName": "bucket_id", - "type": "text", - "isRequired": true, + "id": "auth.refresh_tokens.user_id", + "name": "user_id", + "columnName": "user_id", + "type": "varchar", + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, "sequence": false, "hasDefaultValue": false, "isId": false, - "maxLength": null + "maxLength": 255 }, { - "id": "storage.s3_multipart_uploads_parts.key", - "name": "key", - "columnName": "key", - "type": "text", - "isRequired": true, + "id": "auth.refresh_tokens.revoked", + "name": "revoked", + "columnName": "revoked", + "type": "bool", + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, @@ -4734,11 +4211,11 @@ "maxLength": null }, { - "id": "storage.s3_multipart_uploads_parts.etag", - "name": "etag", - "columnName": "etag", - "type": "text", - "isRequired": true, + "id": "auth.refresh_tokens.created_at", + "name": "created_at", + "columnName": "created_at", + "type": "timestamptz", + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, @@ -4748,10 +4225,10 @@ "maxLength": null }, { - "id": "storage.s3_multipart_uploads_parts.owner_id", - "name": "owner_id", - "columnName": "owner_id", - "type": "text", + "id": "auth.refresh_tokens.updated_at", + "name": "updated_at", + "columnName": "updated_at", + "type": "timestamptz", "isRequired": false, "kind": "scalar", "isList": false, @@ -4762,59 +4239,41 @@ "maxLength": null }, { - "id": "storage.s3_multipart_uploads_parts.version", - "name": "version", - "columnName": "version", - "type": "text", - "isRequired": true, + "id": "auth.refresh_tokens.parent", + "name": "parent", + "columnName": "parent", + "type": "varchar", + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, "sequence": false, "hasDefaultValue": false, "isId": false, - "maxLength": null + "maxLength": 255 }, { - "id": "storage.s3_multipart_uploads_parts.created_at", - "name": "created_at", - "columnName": "created_at", - "type": "timestamptz", - "isRequired": true, + "id": "auth.refresh_tokens.session_id", + "name": "session_id", + "columnName": "session_id", + "type": "uuid", + "isRequired": false, "kind": "scalar", "isList": false, "isGenerated": false, "sequence": false, - "hasDefaultValue": true, + "hasDefaultValue": false, "isId": false, "maxLength": null }, { - "name": "buckets", - "type": "buckets", - "isRequired": true, - "kind": "object", - "relationName": "s3_multipart_uploads_partsTobuckets", - "relationFromFields": [ - "bucket_id" - ], - "relationToFields": [ - "id" - ], - "isList": false, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - }, - { - "name": "s3_multipart_uploads", - "type": "s3_multipart_uploads", - "isRequired": true, + "name": "sessions", + "type": "sessions", + "isRequired": false, "kind": "object", - "relationName": "s3_multipart_uploads_partsTos3_multipart_uploads", + "relationName": "refresh_tokensTosessions", "relationFromFields": [ - "upload_id" + "session_id" ], "relationToFields": [ "id" @@ -4828,11 +4287,18 @@ ], "uniqueConstraints": [ { - "name": "s3_multipart_uploads_parts_pkey", + "name": "refresh_tokens_pkey", "fields": [ "id" ], "nullNotDistinct": false + }, + { + "name": "refresh_tokens_token_unique", + "fields": [ + "token" + ], + "nullNotDistinct": false } ] }, @@ -4953,20 +4419,6 @@ "isId": false, "maxLength": null }, - { - "id": "auth.saml_providers.name_id_format", - "name": "name_id_format", - "columnName": "name_id_format", - "type": "text", - "isRequired": false, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, { "name": "sso_providers", "type": "sso_providers", @@ -5078,6 +4530,20 @@ "isId": false, "maxLength": null }, + { + "id": "auth.saml_relay_states.from_ip_address", + "name": "from_ip_address", + "columnName": "from_ip_address", + "type": "inet", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, { "id": "auth.saml_relay_states.created_at", "name": "created_at", @@ -9432,20 +8898,6 @@ "isId": false, "maxLength": null }, - { - "id": "auth.users.is_anonymous", - "name": "is_anonymous", - "columnName": "is_anonymous", - "type": "bool", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": true, - "isId": false, - "maxLength": null - }, { "name": "identities", "type": "identities", @@ -9474,20 +8926,6 @@ "sequence": false, "hasDefaultValue": false }, - { - "name": "one_time_tokens", - "type": "one_time_tokens", - "isRequired": false, - "kind": "object", - "relationName": "one_time_tokensTousers", - "relationFromFields": [], - "relationToFields": [], - "isList": true, - "isId": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false - }, { "name": "sessions", "type": "sessions", @@ -9996,29 +9434,6 @@ } ] }, - "one_time_token_type": { - "schemaName": "auth", - "values": [ - { - "name": "confirmation_token" - }, - { - "name": "email_change_token_current" - }, - { - "name": "email_change_token_new" - }, - { - "name": "phone_change_token" - }, - { - "name": "reauthentication_token" - }, - { - "name": "recovery_token" - } - ] - }, "request_status": { "schemaName": "net", "values": [ @@ -10105,9 +9520,6 @@ "pg_tle_features": { "schemaName": "pgtle", "values": [ - { - "name": "clientauth" - }, { "name": "passcheck" } diff --git a/packages/temporal/.gitignore b/packages/temporal/.gitignore new file mode 100644 index 000000000..c88144480 --- /dev/null +++ b/packages/temporal/.gitignore @@ -0,0 +1,6 @@ +# Finder (MacOS) folder config +.DS_Store + +/build + +var/** \ No newline at end of file diff --git a/packages/temporal/build/payload-converter.cjs b/packages/temporal/build/payload-converter.cjs new file mode 100644 index 000000000..f8bad8dfe --- /dev/null +++ b/packages/temporal/build/payload-converter.cjs @@ -0,0 +1,882 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/payload-converter.ts +var payload_converter_exports = {}; +__export(payload_converter_exports, { + payloadConverter: () => payloadConverter +}); +module.exports = __toCommonJS(payload_converter_exports); +var import_common2 = require("@temporalio/common"); + +// src/superjson-payload-converter.ts +var import_common = require("@temporalio/common"); + +// ../../node_modules/superjson/dist/double-indexed-kv.js +var DoubleIndexedKV = class { + constructor() { + this.keyToValue = /* @__PURE__ */ new Map(); + this.valueToKey = /* @__PURE__ */ new Map(); + } + set(key, value) { + this.keyToValue.set(key, value); + this.valueToKey.set(value, key); + } + getByKey(key) { + return this.keyToValue.get(key); + } + getByValue(value) { + return this.valueToKey.get(value); + } + clear() { + this.keyToValue.clear(); + this.valueToKey.clear(); + } +}; + +// ../../node_modules/superjson/dist/registry.js +var Registry = class { + constructor(generateIdentifier) { + this.generateIdentifier = generateIdentifier; + this.kv = new DoubleIndexedKV(); + } + register(value, identifier) { + if (this.kv.getByValue(value)) { + return; + } + if (!identifier) { + identifier = this.generateIdentifier(value); + } + this.kv.set(identifier, value); + } + clear() { + this.kv.clear(); + } + getIdentifier(value) { + return this.kv.getByValue(value); + } + getValue(identifier) { + return this.kv.getByKey(identifier); + } +}; + +// ../../node_modules/superjson/dist/class-registry.js +var ClassRegistry = class extends Registry { + constructor() { + super((c) => c.name); + this.classToAllowedProps = /* @__PURE__ */ new Map(); + } + register(value, options) { + if (typeof options === "object") { + if (options.allowProps) { + this.classToAllowedProps.set(value, options.allowProps); + } + super.register(value, options.identifier); + } else { + super.register(value, options); + } + } + getAllowedProps(value) { + return this.classToAllowedProps.get(value); + } +}; + +// ../../node_modules/superjson/dist/util.js +function valuesOfObj(record) { + if ("values" in Object) { + return Object.values(record); + } + const values = []; + for (const key in record) { + if (record.hasOwnProperty(key)) { + values.push(record[key]); + } + } + return values; +} +function find(record, predicate) { + const values = valuesOfObj(record); + if ("find" in values) { + return values.find(predicate); + } + const valuesNotNever = values; + for (let i = 0; i < valuesNotNever.length; i++) { + const value = valuesNotNever[i]; + if (predicate(value)) { + return value; + } + } + return void 0; +} +function forEach(record, run) { + Object.entries(record).forEach(([key, value]) => run(value, key)); +} +function includes(arr, value) { + return arr.indexOf(value) !== -1; +} +function findArr(record, predicate) { + for (let i = 0; i < record.length; i++) { + const value = record[i]; + if (predicate(value)) { + return value; + } + } + return void 0; +} + +// ../../node_modules/superjson/dist/custom-transformer-registry.js +var CustomTransformerRegistry = class { + constructor() { + this.transfomers = {}; + } + register(transformer) { + this.transfomers[transformer.name] = transformer; + } + findApplicable(v) { + return find(this.transfomers, (transformer) => transformer.isApplicable(v)); + } + findByName(name) { + return this.transfomers[name]; + } +}; + +// ../../node_modules/superjson/dist/is.js +var getType = (payload) => Object.prototype.toString.call(payload).slice(8, -1); +var isUndefined = (payload) => typeof payload === "undefined"; +var isNull = (payload) => payload === null; +var isPlainObject = (payload) => { + if (typeof payload !== "object" || payload === null) + return false; + if (payload === Object.prototype) + return false; + if (Object.getPrototypeOf(payload) === null) + return true; + return Object.getPrototypeOf(payload) === Object.prototype; +}; +var isEmptyObject = (payload) => isPlainObject(payload) && Object.keys(payload).length === 0; +var isArray = (payload) => Array.isArray(payload); +var isString = (payload) => typeof payload === "string"; +var isNumber = (payload) => typeof payload === "number" && !isNaN(payload); +var isBoolean = (payload) => typeof payload === "boolean"; +var isRegExp = (payload) => payload instanceof RegExp; +var isMap = (payload) => payload instanceof Map; +var isSet = (payload) => payload instanceof Set; +var isSymbol = (payload) => getType(payload) === "Symbol"; +var isDate = (payload) => payload instanceof Date && !isNaN(payload.valueOf()); +var isError = (payload) => payload instanceof Error; +var isNaNValue = (payload) => typeof payload === "number" && isNaN(payload); +var isPrimitive = (payload) => isBoolean(payload) || isNull(payload) || isUndefined(payload) || isNumber(payload) || isString(payload) || isSymbol(payload); +var isBigint = (payload) => typeof payload === "bigint"; +var isInfinite = (payload) => payload === Infinity || payload === -Infinity; +var isTypedArray = (payload) => ArrayBuffer.isView(payload) && !(payload instanceof DataView); +var isURL = (payload) => payload instanceof URL; + +// ../../node_modules/superjson/dist/pathstringifier.js +var escapeKey = (key) => key.replace(/\./g, "\\."); +var stringifyPath = (path) => path.map(String).map(escapeKey).join("."); +var parsePath = (string) => { + const result = []; + let segment = ""; + for (let i = 0; i < string.length; i++) { + let char = string.charAt(i); + const isEscapedDot = char === "\\" && string.charAt(i + 1) === "."; + if (isEscapedDot) { + segment += "."; + i++; + continue; + } + const isEndOfSegment = char === "."; + if (isEndOfSegment) { + result.push(segment); + segment = ""; + continue; + } + segment += char; + } + const lastSegment = segment; + result.push(lastSegment); + return result; +}; + +// ../../node_modules/superjson/dist/transformer.js +function simpleTransformation(isApplicable, annotation, transform, untransform) { + return { + isApplicable, + annotation, + transform, + untransform + }; +} +var simpleRules = [ + simpleTransformation(isUndefined, "undefined", () => null, () => void 0), + simpleTransformation(isBigint, "bigint", (v) => v.toString(), (v) => { + if (typeof BigInt !== "undefined") { + return BigInt(v); + } + console.error("Please add a BigInt polyfill."); + return v; + }), + simpleTransformation(isDate, "Date", (v) => v.toISOString(), (v) => new Date(v)), + simpleTransformation(isError, "Error", (v, superJson) => { + const baseError = { + name: v.name, + message: v.message + }; + superJson.allowedErrorProps.forEach((prop) => { + baseError[prop] = v[prop]; + }); + return baseError; + }, (v, superJson) => { + const e = new Error(v.message); + e.name = v.name; + e.stack = v.stack; + superJson.allowedErrorProps.forEach((prop) => { + e[prop] = v[prop]; + }); + return e; + }), + simpleTransformation(isRegExp, "regexp", (v) => "" + v, (regex) => { + const body = regex.slice(1, regex.lastIndexOf("/")); + const flags = regex.slice(regex.lastIndexOf("/") + 1); + return new RegExp(body, flags); + }), + simpleTransformation( + isSet, + "set", + // (sets only exist in es6+) + // eslint-disable-next-line es5/no-es6-methods + (v) => [...v.values()], + (v) => new Set(v) + ), + simpleTransformation(isMap, "map", (v) => [...v.entries()], (v) => new Map(v)), + simpleTransformation((v) => isNaNValue(v) || isInfinite(v), "number", (v) => { + if (isNaNValue(v)) { + return "NaN"; + } + if (v > 0) { + return "Infinity"; + } else { + return "-Infinity"; + } + }, Number), + simpleTransformation((v) => v === 0 && 1 / v === -Infinity, "number", () => { + return "-0"; + }, Number), + simpleTransformation(isURL, "URL", (v) => v.toString(), (v) => new URL(v)) +]; +function compositeTransformation(isApplicable, annotation, transform, untransform) { + return { + isApplicable, + annotation, + transform, + untransform + }; +} +var symbolRule = compositeTransformation((s, superJson) => { + if (isSymbol(s)) { + const isRegistered = !!superJson.symbolRegistry.getIdentifier(s); + return isRegistered; + } + return false; +}, (s, superJson) => { + const identifier = superJson.symbolRegistry.getIdentifier(s); + return ["symbol", identifier]; +}, (v) => v.description, (_, a, superJson) => { + const value = superJson.symbolRegistry.getValue(a[1]); + if (!value) { + throw new Error("Trying to deserialize unknown symbol"); + } + return value; +}); +var constructorToName = [ + Int8Array, + Uint8Array, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + Uint8ClampedArray +].reduce((obj, ctor) => { + obj[ctor.name] = ctor; + return obj; +}, {}); +var typedArrayRule = compositeTransformation(isTypedArray, (v) => ["typed-array", v.constructor.name], (v) => [...v], (v, a) => { + const ctor = constructorToName[a[1]]; + if (!ctor) { + throw new Error("Trying to deserialize unknown typed array"); + } + return new ctor(v); +}); +function isInstanceOfRegisteredClass(potentialClass, superJson) { + if (potentialClass?.constructor) { + const isRegistered = !!superJson.classRegistry.getIdentifier(potentialClass.constructor); + return isRegistered; + } + return false; +} +var classRule = compositeTransformation(isInstanceOfRegisteredClass, (clazz, superJson) => { + const identifier = superJson.classRegistry.getIdentifier(clazz.constructor); + return ["class", identifier]; +}, (clazz, superJson) => { + const allowedProps = superJson.classRegistry.getAllowedProps(clazz.constructor); + if (!allowedProps) { + return { ...clazz }; + } + const result = {}; + allowedProps.forEach((prop) => { + result[prop] = clazz[prop]; + }); + return result; +}, (v, a, superJson) => { + const clazz = superJson.classRegistry.getValue(a[1]); + if (!clazz) { + throw new Error(`Trying to deserialize unknown class '${a[1]}' - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564`); + } + return Object.assign(Object.create(clazz.prototype), v); +}); +var customRule = compositeTransformation((value, superJson) => { + return !!superJson.customTransformerRegistry.findApplicable(value); +}, (value, superJson) => { + const transformer = superJson.customTransformerRegistry.findApplicable(value); + return ["custom", transformer.name]; +}, (value, superJson) => { + const transformer = superJson.customTransformerRegistry.findApplicable(value); + return transformer.serialize(value); +}, (v, a, superJson) => { + const transformer = superJson.customTransformerRegistry.findByName(a[1]); + if (!transformer) { + throw new Error("Trying to deserialize unknown custom value"); + } + return transformer.deserialize(v); +}); +var compositeRules = [classRule, symbolRule, customRule, typedArrayRule]; +var transformValue = (value, superJson) => { + const applicableCompositeRule = findArr(compositeRules, (rule) => rule.isApplicable(value, superJson)); + if (applicableCompositeRule) { + return { + value: applicableCompositeRule.transform(value, superJson), + type: applicableCompositeRule.annotation(value, superJson) + }; + } + const applicableSimpleRule = findArr(simpleRules, (rule) => rule.isApplicable(value, superJson)); + if (applicableSimpleRule) { + return { + value: applicableSimpleRule.transform(value, superJson), + type: applicableSimpleRule.annotation + }; + } + return void 0; +}; +var simpleRulesByAnnotation = {}; +simpleRules.forEach((rule) => { + simpleRulesByAnnotation[rule.annotation] = rule; +}); +var untransformValue = (json, type, superJson) => { + if (isArray(type)) { + switch (type[0]) { + case "symbol": + return symbolRule.untransform(json, type, superJson); + case "class": + return classRule.untransform(json, type, superJson); + case "custom": + return customRule.untransform(json, type, superJson); + case "typed-array": + return typedArrayRule.untransform(json, type, superJson); + default: + throw new Error("Unknown transformation: " + type); + } + } else { + const transformation = simpleRulesByAnnotation[type]; + if (!transformation) { + throw new Error("Unknown transformation: " + type); + } + return transformation.untransform(json, superJson); + } +}; + +// ../../node_modules/superjson/dist/accessDeep.js +var getNthKey = (value, n) => { + if (n > value.size) + throw new Error("index out of bounds"); + const keys = value.keys(); + while (n > 0) { + keys.next(); + n--; + } + return keys.next().value; +}; +function validatePath(path) { + if (includes(path, "__proto__")) { + throw new Error("__proto__ is not allowed as a property"); + } + if (includes(path, "prototype")) { + throw new Error("prototype is not allowed as a property"); + } + if (includes(path, "constructor")) { + throw new Error("constructor is not allowed as a property"); + } +} +var getDeep = (object, path) => { + validatePath(path); + for (let i = 0; i < path.length; i++) { + const key = path[i]; + if (isSet(object)) { + object = getNthKey(object, +key); + } else if (isMap(object)) { + const row = +key; + const type = +path[++i] === 0 ? "key" : "value"; + const keyOfRow = getNthKey(object, row); + switch (type) { + case "key": + object = keyOfRow; + break; + case "value": + object = object.get(keyOfRow); + break; + } + } else { + object = object[key]; + } + } + return object; +}; +var setDeep = (object, path, mapper) => { + validatePath(path); + if (path.length === 0) { + return mapper(object); + } + let parent = object; + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]; + if (isArray(parent)) { + const index = +key; + parent = parent[index]; + } else if (isPlainObject(parent)) { + parent = parent[key]; + } else if (isSet(parent)) { + const row = +key; + parent = getNthKey(parent, row); + } else if (isMap(parent)) { + const isEnd = i === path.length - 2; + if (isEnd) { + break; + } + const row = +key; + const type = +path[++i] === 0 ? "key" : "value"; + const keyOfRow = getNthKey(parent, row); + switch (type) { + case "key": + parent = keyOfRow; + break; + case "value": + parent = parent.get(keyOfRow); + break; + } + } + } + const lastKey = path[path.length - 1]; + if (isArray(parent)) { + parent[+lastKey] = mapper(parent[+lastKey]); + } else if (isPlainObject(parent)) { + parent[lastKey] = mapper(parent[lastKey]); + } + if (isSet(parent)) { + const oldValue = getNthKey(parent, +lastKey); + const newValue = mapper(oldValue); + if (oldValue !== newValue) { + parent.delete(oldValue); + parent.add(newValue); + } + } + if (isMap(parent)) { + const row = +path[path.length - 2]; + const keyToRow = getNthKey(parent, row); + const type = +lastKey === 0 ? "key" : "value"; + switch (type) { + case "key": { + const newKey = mapper(keyToRow); + parent.set(newKey, parent.get(keyToRow)); + if (newKey !== keyToRow) { + parent.delete(keyToRow); + } + break; + } + case "value": { + parent.set(keyToRow, mapper(parent.get(keyToRow))); + break; + } + } + } + return object; +}; + +// ../../node_modules/superjson/dist/plainer.js +function traverse(tree, walker2, origin = []) { + if (!tree) { + return; + } + if (!isArray(tree)) { + forEach(tree, (subtree, key) => traverse(subtree, walker2, [...origin, ...parsePath(key)])); + return; + } + const [nodeValue, children] = tree; + if (children) { + forEach(children, (child, key) => { + traverse(child, walker2, [...origin, ...parsePath(key)]); + }); + } + walker2(nodeValue, origin); +} +function applyValueAnnotations(plain, annotations, superJson) { + traverse(annotations, (type, path) => { + plain = setDeep(plain, path, (v) => untransformValue(v, type, superJson)); + }); + return plain; +} +function applyReferentialEqualityAnnotations(plain, annotations) { + function apply(identicalPaths, path) { + const object = getDeep(plain, parsePath(path)); + identicalPaths.map(parsePath).forEach((identicalObjectPath) => { + plain = setDeep(plain, identicalObjectPath, () => object); + }); + } + if (isArray(annotations)) { + const [root, other] = annotations; + root.forEach((identicalPath) => { + plain = setDeep(plain, parsePath(identicalPath), () => plain); + }); + if (other) { + forEach(other, apply); + } + } else { + forEach(annotations, apply); + } + return plain; +} +var isDeep = (object, superJson) => isPlainObject(object) || isArray(object) || isMap(object) || isSet(object) || isInstanceOfRegisteredClass(object, superJson); +function addIdentity(object, path, identities) { + const existingSet = identities.get(object); + if (existingSet) { + existingSet.push(path); + } else { + identities.set(object, [path]); + } +} +function generateReferentialEqualityAnnotations(identitites, dedupe) { + const result = {}; + let rootEqualityPaths = void 0; + identitites.forEach((paths) => { + if (paths.length <= 1) { + return; + } + if (!dedupe) { + paths = paths.map((path) => path.map(String)).sort((a, b) => a.length - b.length); + } + const [representativePath, ...identicalPaths] = paths; + if (representativePath.length === 0) { + rootEqualityPaths = identicalPaths.map(stringifyPath); + } else { + result[stringifyPath(representativePath)] = identicalPaths.map(stringifyPath); + } + }); + if (rootEqualityPaths) { + if (isEmptyObject(result)) { + return [rootEqualityPaths]; + } else { + return [rootEqualityPaths, result]; + } + } else { + return isEmptyObject(result) ? void 0 : result; + } +} +var walker = (object, identities, superJson, dedupe, path = [], objectsInThisPath = [], seenObjects = /* @__PURE__ */ new Map()) => { + const primitive = isPrimitive(object); + if (!primitive) { + addIdentity(object, path, identities); + const seen = seenObjects.get(object); + if (seen) { + return dedupe ? { + transformedValue: null + } : seen; + } + } + if (!isDeep(object, superJson)) { + const transformed2 = transformValue(object, superJson); + const result2 = transformed2 ? { + transformedValue: transformed2.value, + annotations: [transformed2.type] + } : { + transformedValue: object + }; + if (!primitive) { + seenObjects.set(object, result2); + } + return result2; + } + if (includes(objectsInThisPath, object)) { + return { + transformedValue: null + }; + } + const transformationResult = transformValue(object, superJson); + const transformed = transformationResult?.value ?? object; + const transformedValue = isArray(transformed) ? [] : {}; + const innerAnnotations = {}; + forEach(transformed, (value, index) => { + if (index === "__proto__" || index === "constructor" || index === "prototype") { + throw new Error(`Detected property ${index}. This is a prototype pollution risk, please remove it from your object.`); + } + const recursiveResult = walker(value, identities, superJson, dedupe, [...path, index], [...objectsInThisPath, object], seenObjects); + transformedValue[index] = recursiveResult.transformedValue; + if (isArray(recursiveResult.annotations)) { + innerAnnotations[index] = recursiveResult.annotations; + } else if (isPlainObject(recursiveResult.annotations)) { + forEach(recursiveResult.annotations, (tree, key) => { + innerAnnotations[escapeKey(index) + "." + key] = tree; + }); + } + }); + const result = isEmptyObject(innerAnnotations) ? { + transformedValue, + annotations: !!transformationResult ? [transformationResult.type] : void 0 + } : { + transformedValue, + annotations: !!transformationResult ? [transformationResult.type, innerAnnotations] : innerAnnotations + }; + if (!primitive) { + seenObjects.set(object, result); + } + return result; +}; + +// ../../node_modules/is-what/dist/index.js +function getType2(payload) { + return Object.prototype.toString.call(payload).slice(8, -1); +} +function isArray2(payload) { + return getType2(payload) === "Array"; +} +function isPlainObject2(payload) { + if (getType2(payload) !== "Object") + return false; + const prototype = Object.getPrototypeOf(payload); + return !!prototype && prototype.constructor === Object && prototype === Object.prototype; +} +function isNull2(payload) { + return getType2(payload) === "Null"; +} +function isOneOf(a, b, c, d, e) { + return (value) => a(value) || b(value) || !!c && c(value) || !!d && d(value) || !!e && e(value); +} +function isUndefined2(payload) { + return getType2(payload) === "Undefined"; +} +var isNullOrUndefined = isOneOf(isNull2, isUndefined2); + +// ../../node_modules/copy-anything/dist/index.js +function assignProp(carry, key, newVal, originalObject, includeNonenumerable) { + const propType = {}.propertyIsEnumerable.call(originalObject, key) ? "enumerable" : "nonenumerable"; + if (propType === "enumerable") + carry[key] = newVal; + if (includeNonenumerable && propType === "nonenumerable") { + Object.defineProperty(carry, key, { + value: newVal, + enumerable: false, + writable: true, + configurable: true + }); + } +} +function copy(target, options = {}) { + if (isArray2(target)) { + return target.map((item) => copy(item, options)); + } + if (!isPlainObject2(target)) { + return target; + } + const props = Object.getOwnPropertyNames(target); + const symbols = Object.getOwnPropertySymbols(target); + return [...props, ...symbols].reduce((carry, key) => { + if (isArray2(options.props) && !options.props.includes(key)) { + return carry; + } + const val = target[key]; + const newVal = copy(val, options); + assignProp(carry, key, newVal, target, options.nonenumerable); + return carry; + }, {}); +} + +// ../../node_modules/superjson/dist/index.js +var SuperJSON = class { + /** + * @param dedupeReferentialEqualities If true, SuperJSON will make sure only one instance of referentially equal objects are serialized and the rest are replaced with `null`. + */ + constructor({ dedupe = false } = {}) { + this.classRegistry = new ClassRegistry(); + this.symbolRegistry = new Registry((s) => s.description ?? ""); + this.customTransformerRegistry = new CustomTransformerRegistry(); + this.allowedErrorProps = []; + this.dedupe = dedupe; + } + serialize(object) { + const identities = /* @__PURE__ */ new Map(); + const output = walker(object, identities, this, this.dedupe); + const res = { + json: output.transformedValue + }; + if (output.annotations) { + res.meta = { + ...res.meta, + values: output.annotations + }; + } + const equalityAnnotations = generateReferentialEqualityAnnotations(identities, this.dedupe); + if (equalityAnnotations) { + res.meta = { + ...res.meta, + referentialEqualities: equalityAnnotations + }; + } + return res; + } + deserialize(payload) { + const { json, meta } = payload; + let result = copy(json); + if (meta?.values) { + result = applyValueAnnotations(result, meta.values, this); + } + if (meta?.referentialEqualities) { + result = applyReferentialEqualityAnnotations(result, meta.referentialEqualities); + } + return result; + } + stringify(object) { + return JSON.stringify(this.serialize(object)); + } + parse(string) { + return this.deserialize(JSON.parse(string)); + } + registerClass(v, options) { + this.classRegistry.register(v, options); + } + registerSymbol(v, identifier) { + this.symbolRegistry.register(v, identifier); + } + registerCustom(transformer, name) { + this.customTransformerRegistry.register({ + name, + ...transformer + }); + } + allowErrorProps(...props) { + this.allowedErrorProps.push(...props); + } +}; +SuperJSON.defaultInstance = new SuperJSON(); +SuperJSON.serialize = SuperJSON.defaultInstance.serialize.bind(SuperJSON.defaultInstance); +SuperJSON.deserialize = SuperJSON.defaultInstance.deserialize.bind(SuperJSON.defaultInstance); +SuperJSON.stringify = SuperJSON.defaultInstance.stringify.bind(SuperJSON.defaultInstance); +SuperJSON.parse = SuperJSON.defaultInstance.parse.bind(SuperJSON.defaultInstance); +SuperJSON.registerClass = SuperJSON.defaultInstance.registerClass.bind(SuperJSON.defaultInstance); +SuperJSON.registerSymbol = SuperJSON.defaultInstance.registerSymbol.bind(SuperJSON.defaultInstance); +SuperJSON.registerCustom = SuperJSON.defaultInstance.registerCustom.bind(SuperJSON.defaultInstance); +SuperJSON.allowErrorProps = SuperJSON.defaultInstance.allowErrorProps.bind(SuperJSON.defaultInstance); +var serialize = SuperJSON.serialize; +var deserialize = SuperJSON.deserialize; +var stringify = SuperJSON.stringify; +var parse = SuperJSON.parse; +var registerClass = SuperJSON.registerClass; +var registerCustom = SuperJSON.registerCustom; +var registerSymbol = SuperJSON.registerSymbol; +var allowErrorProps = SuperJSON.allowErrorProps; + +// src/superjson-payload-converter.ts +var import_encoding = require("@temporalio/common/lib/encoding"); +var SuperjsonPayloadConverter = class { + // Use 'json/plain' so that Payloads are displayed in the UI + encodingType = "json/plain"; + toPayload(value) { + if (value === void 0) return void 0; + let sjson = ""; + try { + sjson = SuperJSON.stringify(value); + } catch (e) { + throw new UnsupportedSuperjsonTypeError( + `Can't run SUPERJSON.stringify on this value: ${value}. Either convert it (or its properties) to SUPERJSON-serializable values (see https://github.com/flightcontrolhq/superjson#readme ), or create a custom data converter. SJSON.stringify error message: ${errorMessage( + e + )}`, + e + ); + } + return { + metadata: { + [import_common.METADATA_ENCODING_KEY]: (0, import_encoding.encode)("json/plain"), + // Include an additional metadata field to indicate that this is an SuperJSON payload + format: (0, import_encoding.encode)("extended") + }, + data: (0, import_encoding.encode)(sjson) + }; + } + fromPayload(content) { + try { + if (!content.data) { + throw new UnsupportedSuperjsonTypeError( + `Can't run SUPERJSON.parse on this value: ${content.data}. Either convert it (or its properties) to SUPERJSON-serializable values (see https://github.com/flightcontrolhq/superjson#readme ), or create a custom data converter. No data found in payload.` + ); + } + return SuperJSON.parse((0, import_encoding.decode)(content.data)); + } catch (e) { + throw new UnsupportedSuperjsonTypeError( + `Can't run SUPERJSON.parse on this value: ${content.data}. Either convert it (or its properties) to SUPERJSON-serializable values (see https://github.com/flightcontrolhq/superjson#readme ), or create a custom data converter. SJSON.parse error message: ${errorMessage( + e + )}`, + e + ); + } + } +}; +var UnsupportedSuperjsonTypeError = class extends import_common.PayloadConverterError { + constructor(message, cause) { + super(message ?? void 0); + this.cause = cause; + } + name = "UnsupportedJsonTypeError"; +}; +function errorMessage(error) { + if (typeof error === "string") { + return error; + } + if (error instanceof Error) { + return error.message; + } + return void 0; +} + +// src/payload-converter.ts +var payloadConverter = new import_common2.CompositePayloadConverter( + new import_common2.UndefinedPayloadConverter(), + new SuperjsonPayloadConverter() +); +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + payloadConverter +}); diff --git a/packages/temporal/package.json b/packages/temporal/package.json index ce6dd2db2..8b5925e5e 100644 --- a/packages/temporal/package.json +++ b/packages/temporal/package.json @@ -15,17 +15,13 @@ "./client": { "types": "./src/client.ts", "default": "./src/client.ts" - }, - "./superjson-payload-converter": { - "types": "./src/superjson-payload-converter.ts", - "default": "./src/superjson-payload-converter.ts" } }, "scripts": { "lint": "tsc", "server": "temporal server start-dev --db-filename ./var/temporal.db", "build": "esbuild --bundle --outfile=build/payload-converter.cjs --target=esnext --platform=node --external:@temporalio/common --external:@bufbuild/protobuf src/payload-converter.ts" - }, + }, "dependencies": { "@temporalio/client": "^1.10.1", "@temporalio/common": "^1.11.1", diff --git a/packages/temporal/src/client.ts b/packages/temporal/src/client.ts index 298224e21..87bd3f35d 100644 --- a/packages/temporal/src/client.ts +++ b/packages/temporal/src/client.ts @@ -1,5 +1,5 @@ import { Client, Connection } from '@temporalio/client' -import { dataConverter } from './payload-converter' +import { payloadConverter } from './payload-converter' import { createRequire } from 'node:module' const require = createRequire(import.meta.url) import debug from 'debug' @@ -37,7 +37,9 @@ export async function getTemporalClient(): Promise { client = new Client({ connection, namespace: process.env.TEMPORAL_NAMESPACE ?? 'default', - dataConverter: dataConverter, + dataConverter: { + payloadConverterPath: require.resolve('../build/payload-converter.cjs'), + }, }) } return client diff --git a/packages/temporal/tsconfig.json b/packages/temporal/tsconfig.json new file mode 100644 index 000000000..688134c0a --- /dev/null +++ b/packages/temporal/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "composite": true, + "baseUrl": ".", + "paths": { + "@my/temporal": ["../temporal"], + "@my/temporal/*": ["../temporal/*"] + } + }, + "include": ["src/**/*.ts", "../../globals.d.ts", "../../environment.d.ts"] +} diff --git a/packages/workflows/.gitignore b/packages/workflows/.gitignore index 1548a47d0..3c6e5b4df 100644 --- a/packages/workflows/.gitignore +++ b/packages/workflows/.gitignore @@ -175,3 +175,5 @@ dist .DS_Store lib + +workflow-bundle.js \ No newline at end of file diff --git a/packages/workflows/README.md b/packages/workflows/README.md index bba8b12cb..a203ec5ce 100644 --- a/packages/workflows/README.md +++ b/packages/workflows/README.md @@ -1,5 +1,21 @@ # Temporal Workflows +## How to develop Workflow logic + +Workflow logic is constrained by deterministic execution requirements. Therefore, each language is limited to the use of certain idiomatic techniques. However, each Temporal SDK provides a set of APIs that can be used inside your Workflow to interact with external (to the Workflow) application code. + +In the Temporal TypeScript SDK, Workflows run in a deterministic sandboxed environment. The code is bundled on Worker creation using Webpack, and can import any package as long as it does not reference Node.js or DOM APIs. + +> [!NOTE] +> If you must use a library that references a Node.js or DOM API and you are certain that those APIs are not used at runtime, add that module to the ignoreModules list. +> The Workflow sandbox can run only deterministic code, so side effects and access to external state must be done through Activities because Activity outputs are recorded in the Event History and can read deterministically by the Workflow. + +This limitation also means that Workflow code cannot directly import the [Activity Definition](https://docs.temporal.io/activities#activity-definition). [Activity Types](https://docs.temporal.io/activities#activity-type) can be imported, so they can be invoked in a type-safe manner. + +To make the Workflow runtime deterministic, functions like `Math.random()`, `Date`, and `setTimeout()` are replaced by deterministic versions. + +[FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) and [WeakRef](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) are removed because v8's garbage collector is not deterministic. + - **Workflows require one file**: you can organize Workflow code however you like, but each Worker needs to reference a single file that exports all the Workflows it handles (so you have to handle name conflicts instead of us) - **Activities are top level**: - Inside the Temporal Worker, Activities are registered at the same level Workflows are. diff --git a/packages/workflows/package.json b/packages/workflows/package.json index af90b30c5..f344f91c0 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -4,8 +4,9 @@ "package.json", "src" ], + "type": "module", "exports": { - "./activities": { + "./all-activities": { "types": "./src/all-activities.ts", "default": "./src/all-activities.ts" }, @@ -17,15 +18,15 @@ "types": "./src/all-workflows.ts", "default": "./src/all-workflows.ts" }, - "/": { - "types": "./src/all-workflows.ts", - "default": "./src/all-workflows.ts" + "./workflow-bundle": { + "types": "./workflow-bundle.d.ts", + "default": "./workflow-bundle.js" } }, - "type": "module", "scripts": { "lint": "tsc", - "test": "jest" + "test": "jest", + "bundle": "node --loader ts-node/esm --experimental-specifier-resolution=node src/scripts/build-workflow-bundle.ts" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/packages/workflows/src/distribution-workflow/activities.ts b/packages/workflows/src/distribution-workflow/activities.ts index 93b4a3df3..d977f60f0 100644 --- a/packages/workflows/src/distribution-workflow/activities.ts +++ b/packages/workflows/src/distribution-workflow/activities.ts @@ -22,302 +22,302 @@ const inBatches = (array: T[], batchSize = Math.max(8, cpuCount - 1)) => { export function createDistributionActivities(env: Record) { bootstrap(env) -} - -async function fetchAllOpenDistributionsActivity() { - const { data: distributions, error } = await fetchAllOpenDistributions() - if (error) { - if (error.code === 'PGRST116') { - log.info('fetchAllOpenDistributionsActivity', { error }) - return null - } - throw ApplicationFailure.nonRetryable('Error fetching distributions.', error.code, error) - } - log.info('fetchAllOpenDistributionsActivity', { distributions }) - return distributions -} - -async function fetchDistributionActivity(distributionId: string) { - const { data: distribution, error } = await fetchDistribution(distributionId) - if (error) { - if (error.code === 'PGRST116') { - log.info('fetchDistributionActivity', { distributionId, error }) - return null - } - throw ApplicationFailure.nonRetryable('Error fetching distribution.', error.code, error) - } - log.info('fetchDistributionActivity', { distribution }) - return distribution -} - -/** - * Calculates distribution shares for a single distribution. - */ -async function calculateDistributionSharesActivity( - distribution: Tables<'distributions'> & { - distribution_verification_values: Tables<'distribution_verification_values'>[] - } -): Promise { - log.info('calculateDistributionSharesActivity', { distribution }) - // verify tranche is not created when in production - if (await isMerkleDropActive(distribution)) { - throw ApplicationFailure.nonRetryable( - 'Tranche is active. Cannot calculate distribution shares.' - ) - } - - log.info('Calculating distribution shares') - - const { - data: verifications, - error: verificationsError, - count, - } = await fetchAllVerifications(distribution.id) - if (verificationsError) { - throw verificationsError - } - - if (verifications === null || verifications.length === 0) { - log.warn('No verifications found. Skipping distribution.') - return - } + return { + async fetchAllOpenDistributionsActivity() { + const { data: distributions, error } = await fetchAllOpenDistributions() + if (error) { + if (error.code === 'PGRST116') { + log.info('fetchAllOpenDistributionsActivity', { error }) + return null + } + throw ApplicationFailure.nonRetryable('Error fetching distributions.', error.code, error) + } + log.info('fetchAllOpenDistributionsActivity', { distributions }) + return distributions + }, + async fetchDistributionActivity(distributionId: string) { + const { data: distribution, error } = await fetchDistribution(distributionId) + if (error) { + if (error.code === 'PGRST116') { + log.info('fetchDistributionActivity', { distributionId, error }) + return null + } + throw ApplicationFailure.nonRetryable('Error fetching distribution.', error.code, error) + } + log.info('fetchDistributionActivity', { distribution }) + return distribution + }, + /** + * Calculates distribution shares for a single distribution. + */ + async calculateDistributionSharesActivity( + distribution: Tables<'distributions'> & { + distribution_verification_values: Tables<'distribution_verification_values'>[] + } + ): Promise { + log.info('calculateDistributionSharesActivity', { distribution }) + // verify tranche is not created when in production + if (await isMerkleDropActive(distribution)) { + throw ApplicationFailure.nonRetryable( + 'Tranche is active. Cannot calculate distribution shares.' + ) + } - if (count !== verifications.length) { - throw new Error('Verifications count does not match expected count') - } + log.info('Calculating distribution shares') - log.info(`Found ${verifications.length} verifications.`) + const { + data: verifications, + error: verificationsError, + count, + } = await fetchAllVerifications(distribution.id) - const verificationValues = distribution.distribution_verification_values.reduce( - (acc, verification) => { - acc[verification.type] = { - fixedValue: BigInt(verification.fixed_value), - bipsValue: BigInt(verification.bips_value), + if (verificationsError) { + throw verificationsError } - return acc - }, - {} as Record< - Database['public']['Enums']['verification_type'], - { fixedValue?: bigint; bipsValue?: bigint } - > - ) - const verificationsByUserId = verifications.reduce( - (acc, verification) => { - acc[verification.user_id] = acc[verification.user_id] || [] - acc[verification.user_id]?.push(verification) - return acc - }, - {} as Record - ) - log.info(`Found ${Object.keys(verificationsByUserId).length} users with verifications.`) + if (verifications === null || verifications.length === 0) { + log.warn('No verifications found. Skipping distribution.') + return + } - const { data: hodlerAddresses, error: hodlerAddressesError } = await fetchAllHodlers( - distribution.id - ) + if (count !== verifications.length) { + throw new Error('Verifications count does not match expected count') + } - if (hodlerAddressesError) { - throw hodlerAddressesError - } + log.info(`Found ${verifications.length} verifications.`) + + const verificationValues = distribution.distribution_verification_values.reduce( + (acc, verification) => { + acc[verification.type] = { + fixedValue: BigInt(verification.fixed_value), + bipsValue: BigInt(verification.bips_value), + } + return acc + }, + {} as Record< + Database['public']['Enums']['verification_type'], + { fixedValue?: bigint; bipsValue?: bigint } + > + ) + const verificationsByUserId = verifications.reduce( + (acc, verification) => { + acc[verification.user_id] = acc[verification.user_id] || [] + acc[verification.user_id]?.push(verification) + return acc + }, + {} as Record + ) + + log.info(`Found ${Object.keys(verificationsByUserId).length} users with verifications.`) + + const { data: hodlerAddresses, error: hodlerAddressesError } = await fetchAllHodlers( + distribution.id + ) + + if (hodlerAddressesError) { + throw hodlerAddressesError + } - if (hodlerAddresses === null || hodlerAddresses.length === 0) { - throw new Error('No hodler addresses found') - } + if (hodlerAddresses === null || hodlerAddresses.length === 0) { + throw new Error('No hodler addresses found') + } - const hodlerAddressesByUserId = hodlerAddresses.reduce( - (acc, address) => { - acc[address.user_id] = address - return acc - }, - {} as Record - ) - const hodlerUserIdByAddress = hodlerAddresses.reduce( - (acc, address) => { - acc[address.address] = address.user_id - return acc - }, - {} as Record - ) + const hodlerAddressesByUserId = hodlerAddresses.reduce( + (acc, address) => { + acc[address.user_id] = address + return acc + }, + {} as Record + ) + const hodlerUserIdByAddress = hodlerAddresses.reduce( + (acc, address) => { + acc[address.address] = address.user_id + return acc + }, + {} as Record + ) + + log.info(`Found ${hodlerAddresses.length} addresses.`) + + // lookup balances of all hodler addresses in qualification period + const batches = inBatches(hodlerAddresses).flatMap(async (addresses) => { + return await Promise.all( + fetchAllBalances({ + addresses, + distribution, + }) + ) + }) - log.info(`Found ${hodlerAddresses.length} addresses.`) + let minBalanceAddresses: { user_id: string; address: `0x${string}`; balance: string }[] = [] + for await (const batch of batches) { + minBalanceAddresses = minBalanceAddresses.concat(...batch) + } - // lookup balances of all hodler addresses in qualification period - const batches = inBatches(hodlerAddresses).flatMap(async (addresses) => { - return await Promise.all( - fetchAllBalances({ - addresses, - distribution, + log.info(`Found ${minBalanceAddresses.length} balances.`) + // log.debug({ balances }) + + // Filter out hodler with not enough send token balance + minBalanceAddresses = minBalanceAddresses.filter( + ({ balance }) => BigInt(balance) >= BigInt(distribution.hodler_min_balance) + ) + + log.info( + `Found ${minBalanceAddresses.length} balances after filtering hodler_min_balance of ${distribution.hodler_min_balance}` + ) + // log.debug({ balances }) + + // Calculate hodler pool share weights + const distAmt = BigInt(distribution.amount) + const hodlerPoolBips = BigInt(distribution.hodler_pool_bips) + const fixedPoolBips = BigInt(distribution.fixed_pool_bips) + const bonusPoolBips = BigInt(distribution.bonus_pool_bips) + const hodlerPoolAvailableAmount = calculatePercentageWithBips(distAmt, hodlerPoolBips) + const minBalanceByAddress: Record = minBalanceAddresses.reduce( + (acc, balance) => { + acc[balance.address] = BigInt(balance.balance) + return acc + }, + {} as Record + ) + const { totalWeight, weightPerSend, poolWeights, weightedShares } = calculateWeights( + minBalanceAddresses, + hodlerPoolAvailableAmount + ) + + log.info(`Calculated ${Object.keys(poolWeights).length} weights.`, { + totalWeight, + hodlerPoolAvailableAmount, + weightPerSend, }) - ) - }) + // log.debug({ poolWeights }) - let minBalanceAddresses: { user_id: string; address: `0x${string}`; balance: string }[] = [] - for await (const batch of batches) { - minBalanceAddresses = minBalanceAddresses.concat(...batch) - } + if (totalWeight === 0n) { + log.warn('Total weight is 0. Skipping distribution.') + return + } - log.info(`Found ${minBalanceAddresses.length} balances.`) - // log.debug({ balances }) + const fixedPoolAvailableAmount = calculatePercentageWithBips(distAmt, fixedPoolBips) + let fixedPoolAllocatedAmount = 0n + const fixedPoolAmountsByAddress: Record = {} + const bonusPoolBipsByAddress: Record = {} + const maxBonusPoolBips = (bonusPoolBips * PERC_DENOM) / hodlerPoolBips // 3500*10000/6500 = 5384.615384615385% 1.53X - // Filter out hodler with not enough send token balance - minBalanceAddresses = minBalanceAddresses.filter( - ({ balance }) => BigInt(balance) >= BigInt(distribution.hodler_min_balance) - ) + for (const [userId, verifications] of Object.entries(verificationsByUserId)) { + const hodler = hodlerAddressesByUserId[userId] + if (!hodler || !hodler.address) { + continue + } + const { address } = hodler + if (!minBalanceByAddress[address]) { + continue + } + for (const verification of verifications) { + const { fixedValue, bipsValue } = verificationValues[verification.type] + if (fixedValue && fixedPoolAllocatedAmount + fixedValue <= fixedPoolAvailableAmount) { + if (fixedPoolAmountsByAddress[address] === undefined) { + fixedPoolAmountsByAddress[address] = 0n + } + fixedPoolAmountsByAddress[address] += fixedValue + fixedPoolAllocatedAmount += fixedValue + } + if (bipsValue) { + bonusPoolBipsByAddress[address] = (bonusPoolBipsByAddress[address] || 0n) as bigint + bonusPoolBipsByAddress[address] += bipsValue + bonusPoolBipsByAddress[address] = + (bonusPoolBipsByAddress[address] as bigint) > maxBonusPoolBips + ? maxBonusPoolBips + : (bonusPoolBipsByAddress[address] as bigint) // cap at max bonus pool bips + } + } + } - log.info( - `Found ${minBalanceAddresses.length} balances after filtering hodler_min_balance of ${distribution.hodler_min_balance}` - ) - // log.debug({ balances }) - - // Calculate hodler pool share weights - const distAmt = BigInt(distribution.amount) - const hodlerPoolBips = BigInt(distribution.hodler_pool_bips) - const fixedPoolBips = BigInt(distribution.fixed_pool_bips) - const bonusPoolBips = BigInt(distribution.bonus_pool_bips) - const hodlerPoolAvailableAmount = calculatePercentageWithBips(distAmt, hodlerPoolBips) - const minBalanceByAddress: Record = minBalanceAddresses.reduce( - (acc, balance) => { - acc[balance.address] = BigInt(balance.balance) - return acc - }, - {} as Record - ) - const { totalWeight, weightPerSend, poolWeights, weightedShares } = calculateWeights( - minBalanceAddresses, - hodlerPoolAvailableAmount - ) + const hodlerShares = Object.values(weightedShares) + let totalAmount = 0n + let totalHodlerPoolAmount = 0n + let totalBonusPoolAmount = 0n + let totalFixedPoolAmount = 0n - log.info(`Calculated ${Object.keys(poolWeights).length} weights.`, { - totalWeight, - hodlerPoolAvailableAmount, - weightPerSend, - }) - // log.debug({ poolWeights }) + log.info('Calculated fixed & bonus pool amounts.', { + maxBonusPoolBips, + }) - if (totalWeight === 0n) { - log.warn('Total weight is 0. Skipping distribution.') - return - } + const shares = hodlerShares + .map((share) => { + const userId = hodlerUserIdByAddress[share.address] + const bonusBips = bonusPoolBipsByAddress[share.address] || 0n + const hodlerPoolAmount = share.amount + const bonusPoolAmount = calculatePercentageWithBips(hodlerPoolAmount, bonusBips) + const fixedPoolAmount = fixedPoolAmountsByAddress[share.address] || 0n + const amount = hodlerPoolAmount + bonusPoolAmount + fixedPoolAmount + totalAmount += amount + totalHodlerPoolAmount += hodlerPoolAmount + totalBonusPoolAmount += bonusPoolAmount + totalFixedPoolAmount += fixedPoolAmount + + if (!userId) { + log.debug('Hodler not found for address. Skipping share.', { share }) + return null + } + + // log.debug( + // { + // address: share.address, + // balance: balancesByAddress[share.address], + // amount: amount, + // bonusBips, + // hodlerPoolAmount, + // bonusPoolAmount, + // fixedPoolAmount, + // }, + // 'Calculated share.' + // ) + + // @ts-expect-error supabase-js does not support bigint + return { + address: share.address, + distribution_id: distribution.id, + user_id: userId, + amount: amount.toString(), + bonus_pool_amount: bonusPoolAmount.toString(), + fixed_pool_amount: fixedPoolAmount.toString(), + hodler_pool_amount: hodlerPoolAmount.toString(), + } as Tables<'distribution_shares'> + }) + .filter(Boolean) as Tables<'distribution_shares'>[] + + log.info('Distribution totals', { + totalAmount, + totalHodlerPoolAmount, + hodlerPoolAvailableAmount, + totalBonusPoolAmount, + totalFixedPoolAmount, + fixedPoolAllocatedAmount, + fixedPoolAvailableAmount, + maxBonusPoolBips, + name: distribution.name, + shares: shares.length, + }) + log.info(`Calculated ${shares.length} shares.`) - const fixedPoolAvailableAmount = calculatePercentageWithBips(distAmt, fixedPoolBips) - let fixedPoolAllocatedAmount = 0n - const fixedPoolAmountsByAddress: Record = {} - const bonusPoolBipsByAddress: Record = {} - const maxBonusPoolBips = (bonusPoolBips * PERC_DENOM) / hodlerPoolBips // 3500*10000/6500 = 5384.615384615385% 1.53X - - for (const [userId, verifications] of Object.entries(verificationsByUserId)) { - const hodler = hodlerAddressesByUserId[userId] - if (!hodler || !hodler.address) { - continue - } - const { address } = hodler - if (!minBalanceByAddress[address]) { - continue - } - for (const verification of verifications) { - const { fixedValue, bipsValue } = verificationValues[verification.type] - if (fixedValue && fixedPoolAllocatedAmount + fixedValue <= fixedPoolAvailableAmount) { - if (fixedPoolAmountsByAddress[address] === undefined) { - fixedPoolAmountsByAddress[address] = 0n - } - fixedPoolAmountsByAddress[address] += fixedValue - fixedPoolAllocatedAmount += fixedValue + if (totalFixedPoolAmount > fixedPoolAvailableAmount) { + log.warn( + 'Fixed pool amount is greater than available amount. This is not a problem, but it means the fixed pool is exhausted.' + ) } - if (bipsValue) { - bonusPoolBipsByAddress[address] = (bonusPoolBipsByAddress[address] || 0n) as bigint - bonusPoolBipsByAddress[address] += bipsValue - bonusPoolBipsByAddress[address] = - (bonusPoolBipsByAddress[address] as bigint) > maxBonusPoolBips - ? maxBonusPoolBips - : (bonusPoolBipsByAddress[address] as bigint) // cap at max bonus pool bips - } - } - } - const hodlerShares = Object.values(weightedShares) - let totalAmount = 0n - let totalHodlerPoolAmount = 0n - let totalBonusPoolAmount = 0n - let totalFixedPoolAmount = 0n - - log.info('Calculated fixed & bonus pool amounts.', { - maxBonusPoolBips, - }) - - const shares = hodlerShares - .map((share) => { - const userId = hodlerUserIdByAddress[share.address] - const bonusBips = bonusPoolBipsByAddress[share.address] || 0n - const hodlerPoolAmount = share.amount - const bonusPoolAmount = calculatePercentageWithBips(hodlerPoolAmount, bonusBips) - const fixedPoolAmount = fixedPoolAmountsByAddress[share.address] || 0n - const amount = hodlerPoolAmount + bonusPoolAmount + fixedPoolAmount - totalAmount += amount - totalHodlerPoolAmount += hodlerPoolAmount - totalBonusPoolAmount += bonusPoolAmount - totalFixedPoolAmount += fixedPoolAmount - - if (!userId) { - log.debug('Hodler not found for address. Skipping share.', { share }) - return null + // ensure share amounts do not exceed the total distribution amount, ideally this should be done in the database + const totalShareAmounts = shares.reduce((acc, share) => acc + BigInt(share.amount), 0n) + if (totalShareAmounts > distAmt) { + throw new Error('Share amounts exceed total distribution amount') } - // log.debug( - // { - // address: share.address, - // balance: balancesByAddress[share.address], - // amount: amount, - // bonusBips, - // hodlerPoolAmount, - // bonusPoolAmount, - // fixedPoolAmount, - // }, - // 'Calculated share.' - // ) - - // @ts-expect-error supabase-js does not support bigint - return { - address: share.address, - distribution_id: distribution.id, - user_id: userId, - amount: amount.toString(), - bonus_pool_amount: bonusPoolAmount.toString(), - fixed_pool_amount: fixedPoolAmount.toString(), - hodler_pool_amount: hodlerPoolAmount.toString(), - } as Tables<'distribution_shares'> - }) - .filter(Boolean) as Tables<'distribution_shares'>[] - - log.info('Distribution totals', { - totalAmount, - totalHodlerPoolAmount, - hodlerPoolAvailableAmount, - totalBonusPoolAmount, - totalFixedPoolAmount, - fixedPoolAllocatedAmount, - fixedPoolAvailableAmount, - maxBonusPoolBips, - name: distribution.name, - shares: shares.length, - }) - log.info(`Calculated ${shares.length} shares.`) - - if (totalFixedPoolAmount > fixedPoolAvailableAmount) { - log.warn( - 'Fixed pool amount is greater than available amount. This is not a problem, but it means the fixed pool is exhausted.' - ) - } - - // ensure share amounts do not exceed the total distribution amount, ideally this should be done in the database - const totalShareAmounts = shares.reduce((acc, share) => acc + BigInt(share.amount), 0n) - if (totalShareAmounts > distAmt) { - throw new Error('Share amounts exceed total distribution amount') - } - - const { error } = await createDistributionShares(distribution.id, shares) - if (error) { - log.error('Error saving shares.', { error: error.message, code: error.code }) - throw error + const { error } = await createDistributionShares(distribution.id, shares) + if (error) { + log.error('Error saving shares.', { error: error.message, code: error.code }) + throw error + } + }, } } diff --git a/packages/workflows/src/scripts/build-workflow-bundle.ts b/packages/workflows/src/scripts/build-workflow-bundle.ts new file mode 100644 index 000000000..511062d61 --- /dev/null +++ b/packages/workflows/src/scripts/build-workflow-bundle.ts @@ -0,0 +1,21 @@ +import { bundleWorkflowCode } from '@temporalio/worker' +import { writeFile } from 'node:fs/promises' +import path, { dirname } from 'node:path' +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) +import { fileURLToPath } from 'node:url' + +export const __filename = fileURLToPath(import.meta.url) +export const __dirname = dirname(__filename) + +async function bundle() { + const { code } = await bundleWorkflowCode({ + workflowsPath: require.resolve('../all-workflows.ts'), + }) + const codePath = path.join(__dirname, '../../workflow-bundle.js') + + await writeFile(codePath, code) + console.log(`Bundle written to ${codePath}`) +} + +await bundle() diff --git a/packages/workflows/src/transfer-workflow/activities.ts b/packages/workflows/src/transfer-workflow/activities.ts index 67581eb37..f03d997e9 100644 --- a/packages/workflows/src/transfer-workflow/activities.ts +++ b/packages/workflows/src/transfer-workflow/activities.ts @@ -3,75 +3,47 @@ import { isTransferIndexed } from './supabase' import { simulateUserOperation, sendUserOperation, waitForTransactionReceipt } from './wagmi' import type { UserOperation } from 'permissionless' import { bootstrap } from '@my/workflows/utils' +import superjson from 'superjson' export const createTransferActivities = (env: Record) => { bootstrap(env) return { - simulateUserOpActivity, - sendUserOpActivity, - waitForTransactionReceiptActivity, - isTransferIndexedActivity, - } -} -async function simulateUserOpActivity(userOp: UserOperation<'v0.7'>) { - if (!userOp.signature) { - throw ApplicationFailure.nonRetryable('UserOp signature is required') - } - try { - await simulateUserOperation(userOp) - } catch (error) { - throw ApplicationFailure.nonRetryable('Error simulating user operation', error.code, error) - } -} - -async function sendUserOpActivity(userOp: UserOperation<'v0.7'>) { - const creationTime = Date.now() - - try { - const hash = await sendUserOperation(userOp) - log.info('UserOperation sent', { - hash, - sendTime: Date.now(), - userOp: JSON.stringify(userOp, null, 2), - }) - return hash - } catch (error) { - const errorMessage = - error instanceof Error ? `${error.name}: ${error.message}` : 'Unknown error occurred' + async simulateUserOpActivity(userOp: UserOperation<'v0.7'>) { + if (!userOp.signature) { + throw ApplicationFailure.nonRetryable('UserOp signature is required') + } + try { + await simulateUserOperation(userOp) + } catch (error) { + throw ApplicationFailure.nonRetryable('Error simulating user operation', error.code, error) + } + }, + async sendUserOpActivity(userOp: UserOperation<'v0.7'>) { + try { + const hash = await sendUserOperation(userOp) + log.info('sendUserOperationActivity', { hash, userOp: superjson.stringify(userOp) }) + return hash + } catch (error) { + throw ApplicationFailure.nonRetryable('Error sending user operation', error.code, error) + } + }, - log.error('Error in sendUserOpActivity', { - error: errorMessage, - creationTime, - sendTime: Date.now(), - userOp: JSON.stringify(userOp, null, 2), - }) - - throw ApplicationFailure.nonRetryable(errorMessage) - } -} - -async function waitForTransactionReceiptActivity(hash: `0x${string}`) { - if (!hash) { - throw ApplicationFailure.nonRetryable('Invalid hash: hash is undefined') - } - try { - const receipt = await waitForTransactionReceipt(hash) - if (!receipt.success) - throw ApplicationFailure.nonRetryable('Tx failed', receipt.sender, receipt.userOpHash) - log.info('waitForTransactionReceiptActivity', { receipt }) - return receipt - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - log.error('Error in waitForTransactionReceiptActivity', { hash, error: errorMessage }) - throw ApplicationFailure.nonRetryable('Error waiting for tx receipt', errorMessage) - } -} -async function isTransferIndexedActivity(hash: `0x${string}`) { - const isIndexed = await isTransferIndexed(hash) - log.info('isTransferIndexedActivity', { isIndexed }) - if (!isIndexed) { - throw ApplicationFailure.retryable('Transfer not yet indexed in db') + async waitForTransactionReceiptActivity(hash: `0x${string}`) { + try { + const receipt = await waitForTransactionReceipt(hash) + if (!receipt.success) + throw ApplicationFailure.nonRetryable('Tx failed', receipt.sender, receipt.userOpHash) + log.info('waitForTransactionReceiptActivity', { receipt: superjson.stringify(receipt) }) + return receipt + } catch (error) { + throw ApplicationFailure.nonRetryable('Error waiting for tx receipt', error.code, error) + } + }, + async isTransferIndexedActivity(hash: `0x${string}`) { + const isIndexed = await isTransferIndexed(hash) + log.info('isTransferIndexedActivity', { isIndexed }) + return isIndexed + }, } - return isIndexed } diff --git a/packages/workflows/src/transfer-workflow/supabase.ts b/packages/workflows/src/transfer-workflow/supabase.ts index 94c0352e0..f2aeab645 100644 --- a/packages/workflows/src/transfer-workflow/supabase.ts +++ b/packages/workflows/src/transfer-workflow/supabase.ts @@ -3,11 +3,10 @@ import { hexToBytea } from 'app/utils/hexToBytea' import { supabaseAdmin } from 'app/utils/supabase/admin' export async function isTransferIndexed(hash: `0x${string}`) { - const { data, error } = await supabaseAdmin + const { count, error, status, statusText } = await supabaseAdmin .from('send_account_transfers') .select('*', { count: 'exact', head: true }) .eq('tx_hash', hexToBytea(hash)) - .single() log.info('isTransferIndexed', { count, error, status, statusText }) if (error) { @@ -18,8 +17,12 @@ export async function isTransferIndexed(hash: `0x${string}`) { throw ApplicationFailure.nonRetryable( 'Error reading transfer from send_account_transfers column.', error.code, - error + { + ...error, + status, + statusText, + } ) } - return data !== null + return count !== null && count > 0 } diff --git a/packages/workflows/src/transfer-workflow/wagmi.ts b/packages/workflows/src/transfer-workflow/wagmi.ts index eefab60ac..5433d982c 100644 --- a/packages/workflows/src/transfer-workflow/wagmi.ts +++ b/packages/workflows/src/transfer-workflow/wagmi.ts @@ -1,7 +1,9 @@ -import { log, ApplicationFailure } from '@temporalio/activity' import type { UserOperation } from 'permissionless' import { baseMainnetBundlerClient, baseMainnetClient, entryPointAddress } from '@my/wagmi' import type { Hex } from 'viem' +import superjson from 'superjson' + +import { log } from '@temporalio/activity' /** * default user op with preset gas values that work will probably need to move this to the database. @@ -40,7 +42,7 @@ export async function simulateUserOperation(userOp: UserOperation<'v0.7'>) { } export async function sendUserOperation(userOp: UserOperation<'v0.7'>) { - log.info('Sending UserOperation', { userOp: JSON.stringify(userOp, null, 2) }) + log.info('Sending UserOperation', { userOp: superjson.stringify(userOp) }) try { const hash = await baseMainnetBundlerClient.sendUserOperation({ userOperation: userOp, @@ -50,7 +52,7 @@ export async function sendUserOperation(userOp: UserOperation<'v0.7'>) { } catch (error) { log.error('Error in sendUserOperation', { error: error instanceof Error ? error.message : String(error), - userOp: JSON.stringify(userOp, null, 2), + userOp: superjson.stringify(userOp), }) throw error } diff --git a/packages/workflows/src/transfer-workflow/workflow.ts b/packages/workflows/src/transfer-workflow/workflow.ts index e1b423434..0f55be92f 100644 --- a/packages/workflows/src/transfer-workflow/workflow.ts +++ b/packages/workflows/src/transfer-workflow/workflow.ts @@ -16,41 +16,43 @@ const { startToCloseTimeout: '45 seconds', }) -type simulating = { status: 'simulating'; data: { userOp: UserOperation<'v0.7'> } } -type sending = { status: 'sending'; data: { userOp: UserOperation<'v0.7'> } } -type waiting = { status: 'waiting'; data: { hash: string; userOp: UserOperation<'v0.7'> } } -type indexing = { +type BaseState = { userOp: UserOperation<'v0.7'> } + +type Simulating = { status: 'simulating' } & BaseState +type Sending = { status: 'sending' } & BaseState +type Waiting = { status: 'waiting'; hash: string } & BaseState +type Indexing = { status: 'indexing' - data: { receipt: GetUserOperationReceiptReturnType; userOp: UserOperation<'v0.7'> } -} -type confirmed = { + receipt: GetUserOperationReceiptReturnType +} & BaseState +type Confirmed = { status: 'confirmed' receipt: GetUserOperationReceiptReturnType | boolean } & BaseState -export type transferState = simulating | sending | waiting | indexing | confirmed +export type transferState = Simulating | Sending | Waiting | Indexing | Confirmed export const getTransferStateQuery = defineQuery('getTransferState') export async function TransferWorkflow(userOp: UserOperation<'v0.7'>) { - setHandler(getTransferStateQuery, () => ({ status: 'simulating', data: { userOp } })) - log('SendTransferWorkflow started with userOp:', JSON.stringify(parsedUserOp, null, 2)) + setHandler(getTransferStateQuery, () => ({ status: 'simulating', userOp })) + log('SendTransferWorkflow started with userOp:', superjson.stringify(userOp)) await simulateUserOpActivity(userOp) log('Simulation completed') - setHandler(getTransferStateQuery, () => ({ status: 'sending', data: { userOp } })) + setHandler(getTransferStateQuery, () => ({ status: 'sending', userOp })) log('Sending UserOperation') const hash = await sendUserOpActivity(userOp) if (!hash) throw ApplicationFailure.nonRetryable('No hash returned from sendUserOperation') log('UserOperation sent, hash:', hash) - setHandler(getTransferStateQuery, () => ({ status: 'waiting', data: { userOp, hash } })) + setHandler(getTransferStateQuery, () => ({ status: 'waiting', userOp, hash })) const receipt = await waitForTransactionReceiptActivity(hash) if (!receipt) throw ApplicationFailure.nonRetryable('No receipt returned from waitForTransactionReceipt') log('Receipt received:', superjson.stringify(receipt)) - setHandler(getTransferStateQuery, () => ({ status: 'indexing', data: { userOp, receipt } })) - const transfer = await isTransferIndexedActivity(receipt.userOpHash) + setHandler(getTransferStateQuery, () => ({ status: 'indexing', userOp, receipt })) + const transfer = await isTransferIndexedActivity(receipt.receipt.transactionHash) if (!transfer) throw ApplicationFailure.retryable('Transfer not yet indexed in db') log('Transfer indexed:', superjson.stringify(transfer)) - setHandler(getTransferStateQuery, () => ({ status: 'confirmed', data: { userOp, receipt } })) + setHandler(getTransferStateQuery, () => ({ status: 'confirmed', userOp, receipt })) return transfer } diff --git a/packages/workflows/src/utils/bootstrap.ts b/packages/workflows/src/utils/bootstrap.ts new file mode 100644 index 000000000..f5c21b2bf --- /dev/null +++ b/packages/workflows/src/utils/bootstrap.ts @@ -0,0 +1,30 @@ +const requiredEnvVars = [ + 'NEXT_PUBLIC_BASE_CHAIN_ID', + 'NEXT_PUBLIC_BASE_RPC_URL', + 'NEXT_PUBLIC_BUNDLER_RPC_URL', + 'NEXT_PUBLIC_SUPABASE_URL', + 'SUPABASE_DB_URL', + 'SUPABASE_JWT_SECRET', + 'SUPABASE_SERVICE_ROLE', +] as const + +/** + * Bootstraps the workflow by setting up the environment variables that many of our clients depend on. + * This is due to Temporal's deterministic execution requirements. + * + * In the Temporal TypeScript SDK, Workflows run in a deterministic sandboxed environment. + * The code is bundled on Worker creation using Webpack, and can import any package as long as it does not reference Node.js or DOM APIs. + * + * @link https://docs.temporal.io/develop/typescript/core-application#workflow-logic-requirements + */ +export const bootstrap = (env: Record) => { + const varsSet: string[] = [] + for (const envVar of requiredEnvVars) { + if (!env[envVar]) { + throw new Error(`Missing required environment variable: ${envVar}`) + } + varsSet.push(envVar) + globalThis.process.env[envVar] = env[envVar] + } + console.log('Bootstrapped environment variables:', varsSet) +} diff --git a/packages/workflows/src/utils/index.ts b/packages/workflows/src/utils/index.ts new file mode 100644 index 000000000..642ebc387 --- /dev/null +++ b/packages/workflows/src/utils/index.ts @@ -0,0 +1 @@ +export * from './bootstrap' diff --git a/packages/workflows/tsconfig.json b/packages/workflows/tsconfig.json index 16e167628..68b28debc 100644 --- a/packages/workflows/tsconfig.json +++ b/packages/workflows/tsconfig.json @@ -9,19 +9,20 @@ "app/*": ["../app/*"], "@my/wagmi": ["../wagmi/src"], "@my/wagmi/*": ["../wagmi/src/*"], - "@my/api/*": ["../api/src/*"], - "@my/workflows": ["./packages/workflows/src/all-workflows.ts"] + "@my/workflows": ["./src/all-workflows.ts"], + "@my/workflows/*": ["./src/*"], + "@my/temporal": ["../temporal/src"], + "@my/temporal/*": ["../temporal/src/*"] } }, "include": [ "./src", + "../temporal/src", "./jest.config.ts", "../../supabase", "../app", "../wagmi/src", - "../api/src", "../../globals.d.ts", "../../environment.d.ts" - ], - "references": [] + ] } diff --git a/tilt/apps.Tiltfile b/tilt/apps.Tiltfile index 6ef49b0c5..50eb8d121 100644 --- a/tilt/apps.Tiltfile +++ b/tilt/apps.Tiltfile @@ -117,6 +117,7 @@ local_resource( "wagmi:generate", "temporal:build", "temporal", + "workflows:bundle", ], serve_cmd = "yarn workspace workers start", deps = ts_files( diff --git a/tilt/deps.Tiltfile b/tilt/deps.Tiltfile index 42d24a05f..655fbec46 100644 --- a/tilt/deps.Tiltfile +++ b/tilt/deps.Tiltfile @@ -258,6 +258,16 @@ local_resource( deps = ui_files, ) +local_resource( + name="workflows:bundle", + allow_parallel = True, + cmd = "yarn workspace @my/workflows bundle", + labels = labels, + resource_deps = [ + "yarn:install", + ], +) + local_resource( name = "shovel:generate-config", allow_parallel = True, diff --git a/tsconfig.base.json b/tsconfig.base.json index 79af3e529..808e75519 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,6 +13,7 @@ "@my/wagmi": ["./packages/wagmi/src"], "@my/wagmi/*": ["./packages/wagmi/src/*"], "@my/workflows/*": ["./packages/workflows/src/*"], + "@my/temporal/*": ["./packages/temporal/src/*"], "app/*": ["packages/app/*"] }, "importHelpers": true, diff --git a/tsconfig.json b/tsconfig.json index bc6b3ff1f..9e1979ddd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "references": [ { "path": "apps/expo" }, { "path": "apps/next" }, + { "path": "apps/workers" }, { "path": "packages/app" }, { "path": "packages/ui" }, { "path": "packages/api" }, @@ -21,6 +22,7 @@ { "path": "packages/daimo-expo-passkeys" }, { "path": "packages/wagmi" }, { "path": "packages/workflows" }, + { "path": "packages/temporal" }, { "path": "supabase" } ] } diff --git a/yarn.lock b/yarn.lock index 91593cff2..2d9156c95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3126,6 +3126,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/aix-ppc64@npm:0.23.1" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/android-arm64@npm:0.17.18" @@ -3154,6 +3161,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/android-arm64@npm:0.23.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/android-arm@npm:0.17.18" @@ -3182,6 +3196,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/android-arm@npm:0.23.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/android-x64@npm:0.17.18" @@ -3210,6 +3231,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/android-x64@npm:0.23.1" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/darwin-arm64@npm:0.17.18" @@ -3238,6 +3266,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/darwin-arm64@npm:0.23.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/darwin-x64@npm:0.17.18" @@ -3266,6 +3301,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/darwin-x64@npm:0.23.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/freebsd-arm64@npm:0.17.18" @@ -3294,6 +3336,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/freebsd-arm64@npm:0.23.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/freebsd-x64@npm:0.17.18" @@ -3322,6 +3371,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/freebsd-x64@npm:0.23.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-arm64@npm:0.17.18" @@ -3350,6 +3406,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-arm64@npm:0.23.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-arm@npm:0.17.18" @@ -3378,6 +3441,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-arm@npm:0.23.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-ia32@npm:0.17.18" @@ -3406,6 +3476,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-ia32@npm:0.23.1" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-loong64@npm:0.17.18" @@ -3434,6 +3511,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-loong64@npm:0.23.1" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-mips64el@npm:0.17.18" @@ -3462,6 +3546,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-mips64el@npm:0.23.1" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-ppc64@npm:0.17.18" @@ -3490,6 +3581,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-ppc64@npm:0.23.1" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-riscv64@npm:0.17.18" @@ -3518,6 +3616,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-riscv64@npm:0.23.1" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-s390x@npm:0.17.18" @@ -3546,6 +3651,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-s390x@npm:0.23.1" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/linux-x64@npm:0.17.18" @@ -3574,6 +3686,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/linux-x64@npm:0.23.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/netbsd-x64@npm:0.17.18" @@ -3602,6 +3721,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/netbsd-x64@npm:0.23.1" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/openbsd-arm64@npm:0.23.1" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/openbsd-x64@npm:0.17.18" @@ -3630,6 +3763,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/openbsd-x64@npm:0.23.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/sunos-x64@npm:0.17.18" @@ -3658,6 +3798,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/sunos-x64@npm:0.23.1" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/win32-arm64@npm:0.17.18" @@ -3686,6 +3833,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/win32-arm64@npm:0.23.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/win32-ia32@npm:0.17.18" @@ -3714,6 +3868,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/win32-ia32@npm:0.23.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.17.18": version: 0.17.18 resolution: "@esbuild/win32-x64@npm:0.17.18" @@ -3742,6 +3903,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.23.1": + version: 0.23.1 + resolution: "@esbuild/win32-x64@npm:0.23.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -4962,6 +5130,16 @@ __metadata: languageName: node linkType: hard +"@grpc/grpc-js@npm:^1.10.7": + version: 1.12.5 + resolution: "@grpc/grpc-js@npm:1.12.5" + dependencies: + "@grpc/proto-loader": "npm:^0.7.13" + "@js-sdsl/ordered-map": "npm:^4.4.2" + checksum: 10/4f8ead236dcab4d94e15e62d65ad2d93732d37f5cc52ffafe67ae00f69eae4a4c97d6d34a1b9eac9f30206468f2d15302ea6649afcba1d38929afa9d1e7c12d5 + languageName: node + linkType: hard + "@grpc/proto-loader@npm:^0.7.13": version: 0.7.13 resolution: "@grpc/proto-loader@npm:0.7.13" @@ -6227,8 +6405,8 @@ __metadata: debug: "npm:^4.3.6" ms: "npm:^2.1.3" p-queue: "npm:^8.0.1" + superjson: "npm:^2.2.1" permissionless: "npm:^0.1.14" - superjson: "npm:^1.13.1" viem: "npm:^2.18.2" zod: "npm:^3.23.8" languageName: unknown @@ -6318,6 +6496,21 @@ __metadata: languageName: unknown linkType: soft +"@my/temporal@workspace:*, @my/temporal@workspace:packages/temporal": + version: 0.0.0-use.local + resolution: "@my/temporal@workspace:packages/temporal" + dependencies: + "@temporalio/client": "npm:^1.10.1" + "@temporalio/common": "npm:^1.11.1" + esbuild: "npm:^0.23.1" + superjson: "npm:^2.2.1" + temporal: "npm:^0.7.1" + typescript: "npm:^5.5.3" + peerDependencies: + typescript: ^5.5.3 + languageName: unknown + linkType: soft + "@my/ui@workspace:*, @my/ui@workspace:packages/ui": version: 0.0.0-use.local resolution: "@my/ui@workspace:packages/ui" @@ -10959,6 +11152,20 @@ __metadata: languageName: node linkType: hard +"@temporalio/client@npm:^1.10.1": + version: 1.11.5 + resolution: "@temporalio/client@npm:1.11.5" + dependencies: + "@grpc/grpc-js": "npm:^1.10.7" + "@temporalio/common": "npm:1.11.5" + "@temporalio/proto": "npm:1.11.5" + abort-controller: "npm:^3.0.0" + long: "npm:^5.2.3" + uuid: "npm:^9.0.1" + checksum: 10/d77c300dac950e080fb3662be02a2af1fa88c22ebbc374275d85977f2ec5e5fd547c0ca88f6dee5b6264506436823485898097c6fe08c086e239d8bd4e116913 + languageName: node + linkType: hard + "@temporalio/common@npm:1.10.1": version: 1.10.1 resolution: "@temporalio/common@npm:1.10.1" @@ -10971,6 +11178,18 @@ __metadata: languageName: node linkType: hard +"@temporalio/common@npm:1.11.5, @temporalio/common@npm:^1.11.1": + version: 1.11.5 + resolution: "@temporalio/common@npm:1.11.5" + dependencies: + "@temporalio/proto": "npm:1.11.5" + long: "npm:^5.2.3" + ms: "npm:^3.0.0-canary.1" + proto3-json-serializer: "npm:^2.0.0" + checksum: 10/31e90f0fc9520d1ab19cda99e771a8a02dc5531a1320bc963bfb3c46fbca64a1837945188d36624d79d34960790bfe2831f1b862ad805eda02ba270834046c56 + languageName: node + linkType: hard + "@temporalio/core-bridge@npm:1.10.1": version: 1.10.1 resolution: "@temporalio/core-bridge@npm:1.10.1" @@ -11010,6 +11229,16 @@ __metadata: languageName: node linkType: hard +"@temporalio/proto@npm:1.11.5": + version: 1.11.5 + resolution: "@temporalio/proto@npm:1.11.5" + dependencies: + long: "npm:^5.2.3" + protobufjs: "npm:^7.2.5" + checksum: 10/2be4fedc06e0d04e8726e5bab18a055ac3d6197cffd3801ae3b3c3e60e188c05706eefa2fc31c0196e2cfdedf593e23c881c40655e3696cf7bac5f809e67f1e8 + languageName: node + linkType: hard + "@temporalio/testing@npm:^1.10.1": version: 1.10.1 resolution: "@temporalio/testing@npm:1.10.1" @@ -14040,6 +14269,7 @@ __metadata: react-test-renderer: "npm:^18.3.1" react-use-precision-timer: "npm:^3.5.5" solito: "npm:^4.0.1" + superjson: "npm:^2.2.1" superjson: "npm:^1.13.1" type-fest: "npm:^4.32.0" typescript: "npm:^5.5.3" @@ -18911,6 +19141,89 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.23.1": + version: 0.23.1 + resolution: "esbuild@npm:0.23.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.23.1" + "@esbuild/android-arm": "npm:0.23.1" + "@esbuild/android-arm64": "npm:0.23.1" + "@esbuild/android-x64": "npm:0.23.1" + "@esbuild/darwin-arm64": "npm:0.23.1" + "@esbuild/darwin-x64": "npm:0.23.1" + "@esbuild/freebsd-arm64": "npm:0.23.1" + "@esbuild/freebsd-x64": "npm:0.23.1" + "@esbuild/linux-arm": "npm:0.23.1" + "@esbuild/linux-arm64": "npm:0.23.1" + "@esbuild/linux-ia32": "npm:0.23.1" + "@esbuild/linux-loong64": "npm:0.23.1" + "@esbuild/linux-mips64el": "npm:0.23.1" + "@esbuild/linux-ppc64": "npm:0.23.1" + "@esbuild/linux-riscv64": "npm:0.23.1" + "@esbuild/linux-s390x": "npm:0.23.1" + "@esbuild/linux-x64": "npm:0.23.1" + "@esbuild/netbsd-x64": "npm:0.23.1" + "@esbuild/openbsd-arm64": "npm:0.23.1" + "@esbuild/openbsd-x64": "npm:0.23.1" + "@esbuild/sunos-x64": "npm:0.23.1" + "@esbuild/win32-arm64": "npm:0.23.1" + "@esbuild/win32-ia32": "npm:0.23.1" + "@esbuild/win32-x64": "npm:0.23.1" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/f55fbd0bfb0f86ce67a6d2c6f6780729d536c330999ecb9f5a38d578fb9fda820acbbc67d6d1d377eed8fed50fc38f14ff9cb014f86dafab94269a7fb2177018 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -31156,7 +31469,6 @@ __metadata: eslint: "npm:^8.46.0" lefthook: "npm:^1.5.5" node-gyp: "npm:^9.3.1" - snaplet: "npm:^0.42.1" turbo: "npm:^2.1.2" typescript: "npm:^5.5.3" zx: "npm:^8.1.2" @@ -32531,12 +32843,12 @@ __metadata: languageName: node linkType: hard -"superjson@npm:^1.13.1": - version: 1.13.3 - resolution: "superjson@npm:1.13.3" +"superjson@npm:^2.2.1": + version: 2.2.2 + resolution: "superjson@npm:2.2.2" dependencies: copy-anything: "npm:^3.0.2" - checksum: 10/71a186c513a9821e58264c0563cd1b3cf07d3b5ba53a09cc5c1a604d8ffeacac976a6ba1b5d5b3c71b6ab5a1941dfba5a15e3f106ad3ef22fe8d5eee3e2be052 + checksum: 10/6fdc709db4f69d586a18379948e0ade8268c851c791701fea960e29cea12672d7561b4ca89c4049c2e787eb1cec08a51df51d357aa6852078bc0d71d7e17b401 languageName: node linkType: hard @@ -32876,6 +33188,13 @@ __metadata: languageName: node linkType: hard +"temporal@npm:^0.7.1": + version: 0.7.1 + resolution: "temporal@npm:0.7.1" + checksum: 10/ec1b5403229b553577aac55345ea2e6b1445db8628c362ce7ca635e999fce6c3dbbec8f27df5df9341a78ad07e775946730406837d7dd454af7f07596e3c284f + languageName: node + linkType: hard + "tempy@npm:0.3.0": version: 0.3.0 resolution: "tempy@npm:0.3.0" @@ -35465,6 +35784,7 @@ __metadata: "@my/workflows": "workspace:*" "@temporalio/worker": "npm:^1.10.1" "@types/bun": "npm:^1.1.6" + dotenv-cli: "npm:^7.3.0" ts-node: "npm:^10.9.2" typescript: "npm:^5.5.3" peerDependencies: From 435b911b0cbd7d3c6edd0f5ab484fff3b1da2517 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 15 Jan 2025 14:16:54 -0800 Subject: [PATCH 12/15] workflows build command --- apps/workers/src/worker.ts | 2 +- biome.json | 3 ++- package.json | 1 + packages/workflows/package.json | 5 ++++- tilt/apps.Tiltfile | 2 +- tilt/deps.Tiltfile | 4 ++-- yarn.lock | 1 + 7 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/workers/src/worker.ts b/apps/workers/src/worker.ts index 6a9fa3351..3aa6172f7 100644 --- a/apps/workers/src/worker.ts +++ b/apps/workers/src/worker.ts @@ -1,4 +1,4 @@ -import { Worker, NativeConnection, bundleWorkflowCode } from '@temporalio/worker' +import { Worker, NativeConnection } from '@temporalio/worker' import { createTransferActivities } from '@my/workflows/all-activities' import fs from 'node:fs/promises' import { createRequire } from 'node:module' diff --git a/biome.json b/biome.json index cc7f7359d..463609fb8 100644 --- a/biome.json +++ b/biome.json @@ -25,7 +25,8 @@ "./supabase/.temp/**", "./packages/contracts/var/*.json", "**/tsconfig.json", - "**/*.tsconfig.json" + "**/*.tsconfig.json", + "./packages/workflows/workflow-bundle.js" ] }, "organizeImports": { diff --git a/package.json b/package.json index 71b32a100..617acc91b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "snaplet": "yarn workspace @my/snaplet", "workers": "yarn workspace workers", "shovel": "yarn workspace @my/shovel", + "workflows": "yarn workspace @my/workflows", "clean": "yarn workspaces foreach --all -pi run clean" }, "resolutions": { diff --git a/packages/workflows/package.json b/packages/workflows/package.json index f344f91c0..f02fa3bc8 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -26,13 +26,16 @@ "scripts": { "lint": "tsc", "test": "jest", - "bundle": "node --loader ts-node/esm --experimental-specifier-resolution=node src/scripts/build-workflow-bundle.ts" + "build": "yarn bundle", + "bundle": "yarn with-env node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/scripts/build-workflow-bundle.ts", + "with-env": "dotenv -e ../../.env -c --" }, "devDependencies": { "@jest/globals": "^29.7.0", "@temporalio/nyc-test-coverage": "^1.10.1", "@temporalio/testing": "^1.10.1", "@types/source-map-support": "^0", + "dotenv-cli": "^7.3.0", "jest": "^29.7.0", "nyc": "^17.0.0", "source-map-support": "^0.5.21", diff --git a/tilt/apps.Tiltfile b/tilt/apps.Tiltfile index 50eb8d121..2db398e8c 100644 --- a/tilt/apps.Tiltfile +++ b/tilt/apps.Tiltfile @@ -117,7 +117,7 @@ local_resource( "wagmi:generate", "temporal:build", "temporal", - "workflows:bundle", + "workflows:build", ], serve_cmd = "yarn workspace workers start", deps = ts_files( diff --git a/tilt/deps.Tiltfile b/tilt/deps.Tiltfile index 655fbec46..3382ce987 100644 --- a/tilt/deps.Tiltfile +++ b/tilt/deps.Tiltfile @@ -259,9 +259,9 @@ local_resource( ) local_resource( - name="workflows:bundle", + name="workflows:build", allow_parallel = True, - cmd = "yarn workspace @my/workflows bundle", + cmd = "yarn workspace @my/workflows build", labels = labels, resource_deps = [ "yarn:install", diff --git a/yarn.lock b/yarn.lock index 2d9156c95..2b67af4d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6571,6 +6571,7 @@ __metadata: "@temporalio/workflow": "npm:^1.10.1" "@types/source-map-support": "npm:^0" app: "workspace:*" + dotenv-cli: "npm:^7.3.0" jest: "npm:^29.7.0" nyc: "npm:^17.0.0" source-map-support: "npm:^0.5.21" From 6ed4516c8e906d36b55bfd7817672f64567199f9 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 15 Jan 2025 15:22:05 -0800 Subject: [PATCH 13/15] fix client imports --- apps/next/next.config.js | 14 +++++++++++++ .../app/features/home/TokenDetailsHistory.tsx | 1 - packages/app/features/send/confirm/screen.tsx | 20 +++++-------------- packages/temporal/src/client.ts | 5 +---- yarn.lock | 3 +-- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/apps/next/next.config.js b/apps/next/next.config.js index 2155bfe45..89749536e 100644 --- a/apps/next/next.config.js +++ b/apps/next/next.config.js @@ -38,6 +38,20 @@ const plugins = [ }, excludeReactNativeWebExports: ['Switch', 'ProgressBar', 'Picker', 'CheckBox', 'Touchable'], }), + (nextConfig) => { + return { + webpack: (webpackConfig, options) => { + // Add Temporal to externals when building for server + if (options.isServer) { + webpackConfig.externals = [...(webpackConfig.externals || []), '@temporalio/client'] + } + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(webpackConfig, options) + } + return webpackConfig + }, + } + }, (nextConfig) => { return { webpack: (webpackConfig, options) => { diff --git a/packages/app/features/home/TokenDetailsHistory.tsx b/packages/app/features/home/TokenDetailsHistory.tsx index e776b9e9e..3fd8b5e7f 100644 --- a/packages/app/features/home/TokenDetailsHistory.tsx +++ b/packages/app/features/home/TokenDetailsHistory.tsx @@ -18,7 +18,6 @@ export const TokenDetailsHistory = ({ coin }: { coin: CoinWithBalance }) => { (hasPendingTransfers === undefined || hasPendingTransfers) && sendAccount?.address !== undefined, }) - console.log('pendingTransfers: ', pendingTransfers) const { data: pendingTransfersData, isError: pendingTransfersError } = pendingTransfers diff --git a/packages/app/features/send/confirm/screen.tsx b/packages/app/features/send/confirm/screen.tsx index 75ea78ab5..4acc6f3d5 100644 --- a/packages/app/features/send/confirm/screen.tsx +++ b/packages/app/features/send/confirm/screen.tsx @@ -35,9 +35,7 @@ import { useCoin } from 'app/provider/coins' import { useCoinFromSendTokenParam } from 'app/utils/useCoinFromTokenParam' import { allCoinsDict } from 'app/data/coins' import { api } from 'app/utils/api' -import { getUserOperationHash } from 'permissionless' import { signUserOp } from 'app/utils/signUserOp' -import { byteaToBase64 } from 'app/utils/byteaToBase64' import { usePendingTransfers } from 'app/features/home/utils/usePendingTransfers' export function SendConfirmScreen() { @@ -171,7 +169,6 @@ export function SendConfirm() { assert(nonce !== undefined, 'Nonce is not available') throwIf(feesPerGasError) assert(!!feesPerGas, 'Fees per gas is not available') - assert(!!profile?.address, 'Could not resolve recipients send account') assert(selectedCoin?.balance >= BigInt(amount ?? '0'), 'Insufficient balance') const sender = sendAccount?.address as `0x${string}` @@ -186,19 +183,12 @@ export function SendConfirm() { console.log('feesPerGas', feesPerGas) console.log('userOp', _userOp) const chainId = baseMainnetClient.chain.id - const entryPoint = entryPointAddress[chainId] - const userOpHash = getUserOperationHash({ - userOperation: userOp, - entryPoint, - chainId, - }) + const signature = await signUserOp({ - userOpHash, - allowedCredentials: - webauthnCreds?.map((c) => ({ - id: byteaToBase64(c.raw_credential_id), - userHandle: c.name, - })) ?? [], + userOp, + chainId, + webauthnCreds, + entryPoint: entryPointAddress[chainId], }) userOp.signature = signature diff --git a/packages/temporal/src/client.ts b/packages/temporal/src/client.ts index 87bd3f35d..6ec6ca80f 100644 --- a/packages/temporal/src/client.ts +++ b/packages/temporal/src/client.ts @@ -1,7 +1,4 @@ import { Client, Connection } from '@temporalio/client' -import { payloadConverter } from './payload-converter' -import { createRequire } from 'node:module' -const require = createRequire(import.meta.url) import debug from 'debug' import fs from 'node:fs/promises' const { NODE_ENV = 'development' } = process.env @@ -38,7 +35,7 @@ export async function getTemporalClient(): Promise { connection, namespace: process.env.TEMPORAL_NAMESPACE ?? 'default', dataConverter: { - payloadConverterPath: require.resolve('../build/payload-converter.cjs'), + payloadConverterPath: new URL('../build/payload-converter.cjs', import.meta.url).pathname, }, }) } diff --git a/yarn.lock b/yarn.lock index 2b67af4d6..656fae356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6405,8 +6405,8 @@ __metadata: debug: "npm:^4.3.6" ms: "npm:^2.1.3" p-queue: "npm:^8.0.1" - superjson: "npm:^2.2.1" permissionless: "npm:^0.1.14" + superjson: "npm:^2.2.1" viem: "npm:^2.18.2" zod: "npm:^3.23.8" languageName: unknown @@ -14271,7 +14271,6 @@ __metadata: react-use-precision-timer: "npm:^3.5.5" solito: "npm:^4.0.1" superjson: "npm:^2.2.1" - superjson: "npm:^1.13.1" type-fest: "npm:^4.32.0" typescript: "npm:^5.5.3" viem: "npm:^2.18.2" From b3f7d44d01bdaae266e22077e191c849873d62a7 Mon Sep 17 00:00:00 2001 From: musidlo Date: Mon, 20 Jan 2025 22:44:16 +0100 Subject: [PATCH 14/15] Added Ui for notes --- .../components/FormFields/TextAreaField.tsx | 50 ++- packages/app/features/profile/screen.tsx | 27 +- packages/app/features/send/SendAmountForm.tsx | 302 ++++++++++++------ packages/app/features/send/confirm/screen.tsx | 79 +++-- 4 files changed, 311 insertions(+), 147 deletions(-) diff --git a/packages/app/components/FormFields/TextAreaField.tsx b/packages/app/components/FormFields/TextAreaField.tsx index aa9322846..1768a81f5 100644 --- a/packages/app/components/FormFields/TextAreaField.tsx +++ b/packages/app/components/FormFields/TextAreaField.tsx @@ -1,6 +1,6 @@ import { useThemeSetting } from '@tamagui/next-theme' import { useStringFieldInfo, useTsController } from '@ts-react/form' -import { useId } from 'react' +import { forwardRef, type ReactNode, useId } from 'react' import { Fieldset, Label, @@ -12,25 +12,27 @@ import { FieldError, Shake, type LabelProps, + Stack, + useComposedRefs, + type TamaguiElement, } from '@my/ui' -export const TextAreaField = ( - props: Pick< - TextAreaProps, - 'size' | 'autoFocus' | 'aria-label' | 'placeholder' | 'fontStyle' | 'backgroundColor' | 'rows' - > & { labelProps?: LabelProps } -) => { +export const TextAreaField = forwardRef< + TamaguiElement, + TextAreaProps & { labelProps?: LabelProps; iconBefore?: ReactNode; iconAfter?: ReactNode } +>((props, forwardedRef) => { const { field, error, formState: { isSubmitting }, } = useTsController() - const { label, isOptional, placeholder } = useStringFieldInfo() + const { label, placeholder } = useStringFieldInfo() const id = useId() const disabled = isSubmitting const defaultTheme = useThemeName() as string const { resolvedTheme } = useThemeSetting() const themeName = (resolvedTheme ?? defaultTheme) as ThemeName + const composedRefs = useComposedRefs(forwardedRef, field.ref) return ( @@ -45,10 +47,22 @@ export const TextAreaField = ( color={props.labelProps?.color ?? '$olive'} {...props.labelProps} > - {label} {isOptional && '(Optional)'} + {label} )} + {props.iconBefore && ( + + {props.iconBefore} + + )}