diff --git a/apps/site/.gitignore b/apps/site/.gitignore index 2b6827e8..5743d4af 100644 --- a/apps/site/.gitignore +++ b/apps/site/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env*.local .env +.env.* # vercel .vercel diff --git a/apps/site/app/api/post/route.ts b/apps/site/app/api/post/route.ts index 7b0b9524..8efe5d9c 100644 --- a/apps/site/app/api/post/route.ts +++ b/apps/site/app/api/post/route.ts @@ -1,41 +1,47 @@ -import { novaPubClient } from '@/config/publicClient' -import { Defender } from '@openzeppelin/defender-sdk' -import { ethers } from 'ethers' import type { NextRequest } from 'next/server' -import { addresses, postGatewayABI } from 'scrypt' -import type { Hex } from 'viem' +import { + syndicateClient, + generatePostTxnInput, + projectId, +} from '@/config/syndicateClient' +import { waitUntilTx, authToken } from '@/lib' export async function POST(req: NextRequest) { const post = await req.json() - const credentials = { - relayerApiKey: process.env.NONCE_API_UNO, - relayerApiSecret: process.env.NONCE_SECRET_UNO, + if (!syndicateClient) { + return new Response( + JSON.stringify({ + success: false, + hash: null, + error: 'Syndicate client not initialized', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ) } try { - const defenderClient = new Defender(credentials) - const provider = defenderClient.relaySigner.getProvider() - const signer = defenderClient.relaySigner.getSigner(provider, { - speed: 'fast', - }) - - const postGateway = new ethers.Contract( - addresses.postGateway.nova, - postGatewayABI, - signer as unknown as ethers.Signer, - ) + const postTx = + await syndicateClient.officialActions.transact.sendTransaction( + generatePostTxnInput(post), + ) - const tx = await postGateway.post(post) - - await novaPubClient.waitForTransactionReceipt({ - hash: tx.hash as Hex, + const successfulTxHash = await waitUntilTx({ + projectID: projectId as string, + txID: postTx.transactionId, + authToken: authToken as string, }) - return new Response(JSON.stringify({ success: true, hash: tx.hash }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) + return new Response( + JSON.stringify({ success: true, hash: successfulTxHash }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ) } catch (error) { let errorMessage = 'Unknown error' let statusCode = 500 diff --git a/apps/site/app/api/postBatch/route.ts b/apps/site/app/api/postBatch/route.ts index 4a21bc94..47c5e179 100644 --- a/apps/site/app/api/postBatch/route.ts +++ b/apps/site/app/api/postBatch/route.ts @@ -1,41 +1,47 @@ -import { novaPubClient } from '@/config/publicClient' -import { Defender } from '@openzeppelin/defender-sdk' -import { ethers } from 'ethers' import type { NextRequest } from 'next/server' -import { addresses, postGatewayABI } from 'scrypt' -import type { Hex } from 'viem' +import { + syndicateClient, + generatePostBatchTxnInput, + projectId, +} from '@/config/syndicateClient' +import { waitUntilTx, authToken } from '@/lib' export async function POST(req: NextRequest) { const postsArray = await req.json() - console.log({ postsArray }) - const credentials = { - relayerApiKey: process.env.NONCE_API_UNO, - relayerApiSecret: process.env.NONCE_SECRET_UNO, + if (!syndicateClient) { + return new Response( + JSON.stringify({ + success: false, + hash: null, + error: 'Syndicate client not initialized', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ) } try { - const defenderClient = new Defender(credentials) - const provider = defenderClient.relaySigner.getProvider() - const signer = defenderClient.relaySigner.getSigner(provider, { - speed: 'fast', - }) - - const postGateway = new ethers.Contract( - addresses.postGateway.nova, - postGatewayABI, - signer as unknown as ethers.Signer, - ) + const postTx = + await syndicateClient.officialActions.transact.sendTransaction( + generatePostBatchTxnInput(postsArray), + ) - const tx = await postGateway.postBatch(postsArray) - await novaPubClient.waitForTransactionReceipt({ - hash: tx.hash as Hex, + const successfulTxHash = await waitUntilTx({ + projectID: projectId as string, + txID: postTx.transactionId, + authToken: authToken as string, }) - return new Response(JSON.stringify({ success: true, hash: tx.hash }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) + return new Response( + JSON.stringify({ success: true, hash: successfulTxHash }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ) } catch (error) { let errorMessage = 'Unknown error' let statusCode = 500 diff --git a/apps/site/config/syndicateClient.ts b/apps/site/config/syndicateClient.ts new file mode 100644 index 00000000..e2860b69 --- /dev/null +++ b/apps/site/config/syndicateClient.ts @@ -0,0 +1,72 @@ +import { SyndicateClient } from '@syndicateio/syndicate-node' +import { addresses } from 'scrypt' + +type PostMessage = { + rid: bigint + timestamp: bigint + msgType: number + msgBody: string +} + +type Post = { + signer: string + message: PostMessage + hashType: number + hash: string + sigType: number + sig: string +} + +type PostBatchFunction = { + posts: Post[] +} + +export const projectId = process.env.SYNDICATE_PROJECT_ID_POSTGATEWAY + +if (!projectId) { + throw new Error( + 'SYNDICATE_PROJECT_ID_POSTGATEWAY is not defined in environment variables.', + ) +} + +export const generatePostBatchTxnInput = (postsArray: PostBatchFunction) => ({ + projectId: projectId, + contractAddress: addresses.postGateway.nova, + chainId: 42170, + functionSignature: + 'postBatch((address signer, (uint256 rid, uint256 timestamp, uint8 msgType, bytes msgBody) message, uint16 hashType, bytes32 hash, uint16 sigType, bytes sig)[] posts)', + args: { + posts: postsArray, + }, +}) + +export const generatePostTxnInput = (post: Post) => ({ + projectId: projectId, + contractAddress: addresses.postGateway.nova, + chainId: 42170, + functionSignature: + 'post((address signer, (uint256 rid, uint256 timestamp, uint8 msgType, bytes msgBody) message, uint16 hashType, bytes32 hash, uint16 sigType, bytes sig) post)', + args: { + post: post, + }, +}) + +const apiKey = process.env.SYNDICATE_API_KEY + +export const syndicateClient = + !projectId || !apiKey + ? null + : { + officialActions: new SyndicateClient({ + token: () => apiKey, + }), + projectId: projectId, + generatePostTxnInput, + generatePostBatchTxnInput, + } + +if (!projectId || !apiKey) { + throw new Error( + 'Missing SYNDICATE_PROJECT_ID_POSTGATEWAY or SYNDICATE_API_KEY in environment variables.', + ) +} diff --git a/apps/site/lib/api.ts b/apps/site/lib/api.ts index 3e78e61a..e155eb8c 100644 --- a/apps/site/lib/api.ts +++ b/apps/site/lib/api.ts @@ -21,6 +21,102 @@ type User = { sig: string } +export interface TransactionAttempt { + block: number + blockCreatedAt: string + chainId: number + createdAt: string + hash: string + nonce: number + reverted: boolean + signedTxn: string + status: string + transactionId: string + updatedAt: string + walletAddress: string +} + +export interface SyndicateApiResponse { + chainId: number + contractAddress: string + createdAt: string + data: string + decodedData: object + functionSignature: string + invalid: boolean + projectId: string + transactionAttempts: TransactionAttempt[] + transactionId: string + updatedAt: string + value: string +} + +export interface WaitUntilTxOptions { + projectID: string + txID: string + authToken: string + maxAttempts?: number + every?: number +} + +export const authToken = process.env.SYNDICATE_API_KEY + +export const getTransactionRequest = async ({ + projectID, + txID, + authToken, +}: Pick & { authToken: string }) => { + const response = await fetch( + `https://api.syndicate.io/wallet/project/${projectID}/request/${txID}`, + { + method: 'GET', + headers: { Authorization: `Bearer ${authToken}` }, + }, + ) + if (!response.ok) { + throw new Error(`Failed to get transaction request: ${response.statusText}`) + } + return response.json() +} + +export async function waitUntilTx({ + projectID, + txID, + authToken, + maxAttempts = 20, + every = 1000, +}: WaitUntilTxOptions) { + let currAttempts = 0 + let transactionHash = null + + while (!transactionHash && currAttempts < maxAttempts) { + const txAttempts = ( + await getTransactionRequest({ projectID, txID, authToken }) + )?.transactionAttempts + + console.log({ txAttempts }) + + if (txAttempts && txAttempts.length > 0) { + const lastAttempt = txAttempts[txAttempts.length - 1] + if (lastAttempt.status === 'PENDING' && !lastAttempt.reverted) { + transactionHash = lastAttempt.hash + break + } + } + + currAttempts++ + if (!transactionHash && currAttempts < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, every)) + } + } + + if (!transactionHash) { + throw new Error('Transaction not found within maximum attempts') + } + + return transactionHash +} + /* API ROUTES */ // This is in to help with serialization of bigints during json stringify diff --git a/apps/site/package.json b/apps/site/package.json index b801decb..bbf635c8 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@syndicateio/syndicate-node": "^0.0.449", "@vercel/analytics": "^1.0.2", "@vercel/kv": "^1.0.0", "base64url": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d94d2ff4..b3c114f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.21)(react@18.2.0) + '@syndicateio/syndicate-node': + specifier: ^0.0.449 + version: 0.0.449 '@vercel/analytics': specifier: ^1.0.2 version: 1.0.2 @@ -4927,6 +4930,18 @@ packages: tslib: 2.6.2 dev: false + /@syndicateio/syndicate-node@0.0.449: + resolution: {integrity: sha512-pORxSFqNqxwt9lUUbuAjjtE3AzWaKlOCiNxmLOCF8ljAN2VjOjkAWXIa7mXpBzDF2KrR+PKT3VsKMlIRYgMb5w==} + dependencies: + '@types/url-join': 4.0.1 + '@ungap/url-search-params': 0.2.2 + axios: 0.27.2 + js-base64: 3.7.2 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + dev: false + /@szmarczak/http-timer@4.0.6: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -5206,6 +5221,10 @@ packages: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} dev: false + /@types/url-join@4.0.1: + resolution: {integrity: sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ==} + dev: false + /@types/ws@8.5.9: resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} dependencies: @@ -5216,6 +5235,10 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false + /@ungap/url-search-params@0.2.2: + resolution: {integrity: sha512-qQsguKXZVKdCixOHX9jqnX/K/1HekPDpGKyEcXHT+zR6EjGA7S4boSuelL4uuPv6YfhN0n8c4UxW+v/Z3gM2iw==} + dev: false + /@upstash/redis@1.24.3: resolution: {integrity: sha512-gw6d4IA1biB4eye5ESaXc0zOlVQI94aptsBvVcTghYWu1kRmOrJFoMFEDCa8p5uzluyYAOFCuY2GWLR6O4ZoIw==} dependencies: @@ -6160,6 +6183,15 @@ packages: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} dev: false + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.6.7: resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: @@ -9721,6 +9753,10 @@ packages: engines: {node: '>=10'} dev: false + /js-base64@3.7.2: + resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==} + dev: false + /js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} dev: false @@ -14217,6 +14253,10 @@ packages: punycode: 2.3.1 dev: false + /url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + dev: false + /url-set-query@1.0.0: resolution: {integrity: sha512-3AChu4NiXquPfeckE5R5cGdiHCMWJx1dwCWOmWIL4KHAziJNOFIYJlpGFeKDvwLPHovZRCxK3cYlwzqI9Vp+Gg==} dev: false