Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first pass at syndicate integration #549

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/site/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*
# local env files
.env*.local
.env
.env.*

# vercel
.vercel
Expand Down
60 changes: 33 additions & 27 deletions apps/site/app/api/post/route.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
60 changes: 33 additions & 27 deletions apps/site/app/api/postBatch/route.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
72 changes: 72 additions & 0 deletions apps/site/config/syndicateClient.ts
Original file line number Diff line number Diff line change
@@ -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.',
)
}
96 changes: 96 additions & 0 deletions apps/site/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WaitUntilTxOptions, 'projectID' | 'txID'> & { 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
Expand Down
1 change: 1 addition & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading