From 571a8a141c6ef4bdaf8c49b462576ea31850f772 Mon Sep 17 00:00:00 2001 From: Kevin Peters <kevin@w7.xyz> Date: Mon, 30 Oct 2023 10:24:05 -0500 Subject: [PATCH 1/3] cloud-functions: Added getSolanaEvents cloud function The purpose of this cloud function is to fetch Solana token bridge transfer events for a given slot range. --- cloud_functions/package.json | 4 +- cloud_functions/scripts/deploy.sh | 7 + cloud_functions/src/getSolanaEvents.ts | 363 +++++++++++++++++++++++++ cloud_functions/src/index.ts | 2 + package-lock.json | 324 ++++++++++------------ 5 files changed, 512 insertions(+), 188 deletions(-) create mode 100644 cloud_functions/src/getSolanaEvents.ts diff --git a/cloud_functions/package.json b/cloud_functions/package.json index 7b2d3812..3752895f 100644 --- a/cloud_functions/package.json +++ b/cloud_functions/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "dev": "ts-node src/index.ts", - "start": "npx functions-framework --target=wormchainMonitor [--signature-type=http]", + "start": "npx functions-framework --target=getSolanaEvents [--signature-type=http]", "deploy": "bash scripts/deploy.sh", "gcp-build": "npm i ./dist/src/wormhole-foundation-wormhole-monitor-common-0.0.1.tgz ./dist/src/wormhole-foundation-wormhole-monitor-database-0.0.1.tgz" }, @@ -18,7 +18,9 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", + "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", + "borsh": "^1.0.0", "dotenv": "^16.0.3", "firebase-admin": "^11.4.1", "knex": "^2.4.2", diff --git a/cloud_functions/scripts/deploy.sh b/cloud_functions/scripts/deploy.sh index 0b760c20..aab28384 100755 --- a/cloud_functions/scripts/deploy.sh +++ b/cloud_functions/scripts/deploy.sh @@ -211,6 +211,12 @@ if [ -z "$WORMCHAIN_PAGERDUTY_URL" ]; then echo "WORMCHAIN_PAGERDUTY_URL must be specified" exit 1 fi + +if [ -z "$SOLANA_RPC" ]; then + echo "SOLANA_RPC must be specified" + exit 1 +fi + gcloud functions --project "$GCP_PROJECT" deploy compute-tvl --entry-point computeTVL --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 1GB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_ATTEST_MESSAGE_TABLE=$PG_ATTEST_MESSAGE_TABLE,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_TRANSFER_TABLE=$PG_TOKEN_TRANSFER_TABLE,FIRESTORE_TVL_COLLECTION=$FIRESTORE_TVL_COLLECTION gcloud functions --project "$GCP_PROJECT" deploy compute-tvl-history --entry-point computeTVLHistory --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 540 --memory 1GB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_ATTEST_MESSAGE_TABLE=$PG_ATTEST_MESSAGE_TABLE,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_TRANSFER_TABLE=$PG_TOKEN_TRANSFER_TABLE,FIRESTORE_TVL_HISTORY_COLLECTION=$FIRESTORE_TVL_HISTORY_COLLECTION,PG_TOKEN_PRICE_HISTORY_TABLE=$PG_TOKEN_PRICE_HISTORY_TABLE gcloud functions --project "$GCP_PROJECT" deploy compute-tvl-tvm --entry-point computeTvlTvm --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 540 --memory 1GB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_PRICE_HISTORY_TABLE=$PG_TOKEN_PRICE_HISTORY_TABLE,FIRESTORE_LATEST_TVLTVM_COLLECTION=$FIRESTORE_LATEST_TVLTVM_COLLECTION @@ -219,6 +225,7 @@ gcloud functions --project "$GCP_PROJECT" deploy process-vaa --entry-point proce gcloud functions --project "$GCP_PROJECT" deploy refresh-todays-token-prices --entry-point refreshTodaysTokenPrices --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_PRICE_HISTORY_TABLE=$PG_TOKEN_PRICE_HISTORY_TABLE gcloud functions --project "$GCP_PROJECT" deploy update-token-metadata --entry-point updateTokenMetadata --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE gcloud functions --project "$GCP_PROJECT" deploy wormchain-monitor --entry-point wormchainMonitor --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars WORMCHAIN_SLACK_CHANNEL_ID=$WORMCHAIN_SLACK_CHANNEL_ID,WORMCHAIN_SLACK_POST_URL=$WORMCHAIN_SLACK_POST_URL,WORMCHAIN_SLACK_BOT_TOKEN=$WORMCHAIN_SLACK_BOT_TOKEN,WORMCHAIN_PAGERDUTY_ROUTING_KEY=$WORMCHAIN_PAGERDUTY_ROUTING_KEY,WORMCHAIN_PAGERDUTY_URL=$WORMCHAIN_PAGERDUTY_URL +gcloud functions --project "$GCP_PROJECT" deploy get-solana-events --entry-point getSolanaEvents --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars SOLANA_RPC=$SOLANA_RPC if [ "$NETWORK" == "MAINNET" ]; then echo "Finished deploying MAINNET functions" diff --git a/cloud_functions/src/getSolanaEvents.ts b/cloud_functions/src/getSolanaEvents.ts new file mode 100644 index 00000000..bac1fda9 --- /dev/null +++ b/cloud_functions/src/getSolanaEvents.ts @@ -0,0 +1,363 @@ +import { + Connection, + ConfirmedSignatureInfo, + PublicKey, + ParsedInstruction, + PartiallyDecodedInstruction, + ParsedTransactionWithMeta, + SolanaJSONRPCError, + VersionedBlockResponse, +} from '@solana/web3.js'; +import * as ethers from 'ethers'; +import * as bs58 from 'bs58'; +import { deserialize } from 'borsh'; +import { assertEnvironmentVariable } from '@wormhole-foundation/wormhole-monitor-common'; + +interface EventData { + blockNumber: number; + txHash: string; + from: string; + to: string; + token: string; + amount: string; + isDeposit: boolean; +} + +export async function getSolanaEvents(req: any, res: any) { + res.set('Access-Control-Allow-Origin', '*'); + if (req.method === 'OPTIONS') { + // Send response to OPTIONS requests + res.set('Access-Control-Allow-Methods', 'GET'); + res.set('Access-Control-Allow-Headers', 'Content-Type'); + res.set('Access-Control-Max-Age', '3600'); + res.sendStatus(204); + return; + } + if (!req.query.fromSlot) { + res.status(400).send('fromSlot is required'); + return; + } + if (!req.query.toSlot) { + res.status(400).send('toSlot is required'); + return; + } + try { + const fromSlot = Number(req.query.fromSlot); + const toSlot = Number(req.query.toSlot); + console.log(`fetching events from ${fromSlot} to ${toSlot}`); + // the RPC doesn't store blocks that are too old + const events = fromSlot < 232090284 ? [] : await _getSolanaEvents(fromSlot, toSlot); + console.log(`fetched ${events.length} events`); + res.json(events); + } catch (e) { + console.error(e); + res.sendStatus(500); + } +} + +const coreBridge = new PublicKey('worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth'); +const tokenBridge = new PublicKey('wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb'); +const tokenProgram = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + +const mintAuthority = 'BCD75RNBHrJJpW4dXVagL5mPjzRLnVZq4YirJdjEYMV7'; +const transferAuthority = '7oPa2PHQdZmjSPqvpZN7MQxnC7Dcf3uL4oLqknGLk2S3'; +const custodyAuthority = 'GugU1tP7doLeTw9hQP51xRJyS8Da1fWxuiy2rVrnMD2m'; + +enum CoreBridgeIxId { + PostMessage = 0x1, +} + +enum TokenBridgeIxId { + CompleteNative = 0x2, + CompleteWrapped, + TransferWrapped, + TransferNative, + CompleteNativeWithPayload = 0x9, + CompleteWrappedWithPayload, + TransferWrappedWithPayload, + TransferNativeWithPayload, +} + +const PostMessageDataSchema = { + struct: { + nonce: 'u32', + payload: { array: { type: 'u8' } }, + consistency_level: 'u8', + }, +}; + +/** + * Retrieves Solana events from a given slot range using the token bridge. + * @param fromSlot The starting slot to retrieve events from. + * @param toSlot The ending slot to retrieve events from. + * @returns An array of EventData objects representing the events that occurred within the given slot range. + */ +const _getSolanaEvents = async (fromSlot: number, toSlot: number) => { + const txs = await getParsedTransactions(fromSlot, toSlot, tokenBridge); + const events = txs.reduce((acc, tx) => { + if (!tx || !tx.blockTime || tx.meta?.err) { + return acc; + } + // handle the case where the token bridge instruction is a top-level instruction + tx.transaction.message.instructions.forEach((ix: any, index: any) => { + if (!isTokenBridgeIx(ix)) { + return; + } + const innerIx = tx.meta?.innerInstructions?.find((innerIx: any) => innerIx.index === index); + if (!innerIx || innerIx.instructions.length === 0) { + return; + } + const event = getEventData(tx, ix, innerIx.instructions); + if (event) { + acc.push(event); + } + }); + // handle the case where the token bridge instruction is an inner instruction + tx.meta?.innerInstructions?.forEach((innerIx: any) => { + innerIx.instructions.forEach((ix: any, index: any) => { + if (isTokenBridgeIx(ix)) { + const event = getEventData(tx, ix, innerIx.instructions.slice(index + 1)); + if (event) { + acc.push(event); + } + } + }); + }); + return acc; + }, [] as EventData[]); + return events; +}; + +const getEventData = ( + tx: ParsedTransactionWithMeta, + tokenBridgeIx: PartiallyDecodedInstruction, + innerIxs: (ParsedInstruction | PartiallyDecodedInstruction)[] +): EventData | undefined => { + const data = bs58.decode(tokenBridgeIx.data); + if (data.length === 0) { + return; + } + const tokenBridgeIxId = data[0]; + const txHash = tx.transaction.signatures[0]; + const blockNumber = tx.slot; + // search the inner instructions for token transfer instructions to get the event data + switch (tokenBridgeIxId) { + case TokenBridgeIxId.TransferNative: + case TokenBridgeIxId.TransferNativeWithPayload: { + const transferIx = innerIxs.find( + (ix): ix is ParsedInstruction => + isTransferIx(ix) && ix.parsed.info?.authority === transferAuthority + ); + if (transferIx) { + return { + blockNumber, + txHash, + to: transferIx.parsed.info?.destination, + from: transferIx.parsed.info?.source, + token: tokenBridgeIx.accounts[3]?.toString() || '', // mint account + amount: transferIx.parsed.info?.amount, + isDeposit: true, + }; + } + break; + } + case TokenBridgeIxId.TransferWrapped: + case TokenBridgeIxId.TransferWrappedWithPayload: { + const burnIx = innerIxs.find( + (ix): ix is ParsedInstruction => + isBurnIx(ix) && ix.parsed.info?.authority === transferAuthority + ); + const coreBridgeIx = innerIxs.find( + (ix): ix is PartiallyDecodedInstruction => + ix.programId.equals(coreBridge) && (ix as PartiallyDecodedInstruction).data !== undefined + ); + const coreBridgeIxData = coreBridgeIx?.data + ? Buffer.from(bs58.decode(coreBridgeIx.data)) + : undefined; + if ( + burnIx && + coreBridgeIxData && + coreBridgeIxData.length > 0 && + coreBridgeIxData[0] === CoreBridgeIxId.PostMessage + ) { + const postMessageData: any = deserialize( + PostMessageDataSchema, + coreBridgeIxData.subarray(1) + ); + const payload = Buffer.from(postMessageData.payload); + const originChain = payload.readUint16BE(65); + const toChain = payload.readUInt16BE(99); + // if this is a wrapped token being burned and not being sent to its origin chain, + // then it should be included in the volume by fixing the `to` address + // https://docs.wormhole.com/wormhole/explore-wormhole/vaa#token-transfer + const to = toChain !== originChain ? tokenBridge.toString() : ethers.constants.AddressZero; + return { + blockNumber, + txHash, + to, + from: burnIx.parsed.info?.account, + token: tokenBridgeIx.accounts[4]?.toString() || '', // mint account + amount: burnIx.parsed.info?.amount, + isDeposit: false, + }; + } + break; + } + case TokenBridgeIxId.CompleteNative: + case TokenBridgeIxId.CompleteNativeWithPayload: { + // TODO: this doesn't handle the case where the fee recipient is not the destination + // in this case there will be another transfer instruction with the fee recipient as the destination + const transferIx = innerIxs.find( + (ix): ix is ParsedInstruction => + isTransferIx(ix) && + ix.parsed.info?.authority === custodyAuthority && + (tokenBridgeIxId === TokenBridgeIxId.CompleteNativeWithPayload || + ix.parsed.info?.destination === tokenBridgeIx.accounts[6].toString()) + ); + if (transferIx) { + const mintAccountIndex = tokenBridgeIxId === TokenBridgeIxId.CompleteNative ? 8 : 9; + return { + blockNumber, + txHash, + to: transferIx.parsed.info?.destination, + from: transferIx.parsed.info?.source, + token: tokenBridgeIx.accounts[mintAccountIndex]?.toString() || '', // mint account + amount: transferIx.parsed.info?.amount, + isDeposit: false, + }; + } + break; + } + case TokenBridgeIxId.CompleteWrapped: + case TokenBridgeIxId.CompleteWrappedWithPayload: { + // TODO: this doesn't handle the case where the fee recipient is not the destination + // in this case there will be another mint instruction with the fee recipient as the destination + const mintToIx = innerIxs.find( + (ix): ix is ParsedInstruction => + isMintToIx(ix) && + ix.parsed.info?.mintAuthority === mintAuthority && + (tokenBridgeIxId === TokenBridgeIxId.CompleteWrappedWithPayload || + ix.parsed.info?.account === tokenBridgeIx.accounts[6].toString()) + ); + if (mintToIx) { + return { + blockNumber, + txHash, + to: mintToIx.parsed.info?.account, + // to be consistent with the ethereum adapter, + // we set the `from` address to the zero address for minted tokens + from: ethers.constants.AddressZero, + token: mintToIx.parsed.info?.mint, + amount: mintToIx.parsed.info?.amount, + isDeposit: false, + }; + } + break; + } + } +}; + +const isTokenBridgeIx = ( + ix: ParsedInstruction | PartiallyDecodedInstruction +): ix is PartiallyDecodedInstruction => + ix.programId.equals(tokenBridge) && (ix as PartiallyDecodedInstruction).accounts !== undefined; + +const isMintToIx = (ix: ParsedInstruction | PartiallyDecodedInstruction): ix is ParsedInstruction => + isTxOfType(ix, tokenProgram, 'mintTo'); + +const isBurnIx = (ix: ParsedInstruction | PartiallyDecodedInstruction): ix is ParsedInstruction => + isTxOfType(ix, tokenProgram, 'burn'); + +const isTransferIx = ( + ix: ParsedInstruction | PartiallyDecodedInstruction +): ix is ParsedInstruction => isTxOfType(ix, tokenProgram, 'transfer'); + +const isTxOfType = ( + ix: ParsedInstruction | PartiallyDecodedInstruction, + programId: PublicKey, + type: string +) => (ix as ParsedInstruction).parsed?.type === type && ix.programId.equals(programId); + +/** + * Fetches and returns an array of parsed transactions for a given address within a specified block range. + * @param fromSlot The starting block slot. + * @param toSlot The ending block slot. + * @param address The public key of the address to fetch transactions for. + * @returns An array of parsed transactions within the specified block range. + * @throws An error if the block range is invalid or too large, or if a transaction cannot be fetched. + */ +async function getParsedTransactions( + fromSlot: number, + toSlot: number, + address: PublicKey +): Promise<(ParsedTransactionWithMeta | null)[]> { + const connection = new Connection(assertEnvironmentVariable('SOLANA_RPC')); + if (fromSlot > toSlot) throw new Error('invalid block range'); + if (toSlot - fromSlot > 100_000) throw new Error('block range too large'); + + // identify block range by fetching signatures of the first and last transactions + // getSignaturesForAddress walks backwards so fromSignature occurs after toSignature + let toBlock: VersionedBlockResponse | null = null; + try { + toBlock = await connection.getBlock(toSlot, { + maxSupportedTransactionVersion: 0, + }); + } catch (e) { + if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) { + // failed to get confirmed block: slot was skipped or missing in long-term storage + return getParsedTransactions(fromSlot, toSlot - 1, address); + } else { + throw e; + } + } + if (!toBlock || !toBlock.blockTime || toBlock.transactions.length === 0) { + return getParsedTransactions(fromSlot, toSlot - 1, address); + } + const fromSignature = + toBlock.transactions[toBlock.transactions.length - 1].transaction.signatures[0]; + + let fromBlock: VersionedBlockResponse | null = null; + try { + fromBlock = await connection.getBlock(fromSlot, { + maxSupportedTransactionVersion: 0, + }); + } catch (e) { + if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) { + // failed to get confirmed block: slot was skipped or missing in long-term storage + return getParsedTransactions(fromSlot + 1, toSlot, address); + } else { + throw e; + } + } + if (!fromBlock || !fromBlock.blockTime || fromBlock.transactions.length === 0) { + return getParsedTransactions(fromSlot + 1, toSlot, address); + } + const toSignature = fromBlock.transactions[0].transaction.signatures[0]; + + // get all `address` signatures between fromTransaction and toTransaction + const results = []; + let numSignatures = 0; + let currSignature: string | undefined = fromSignature; + const limit = 100; + do { + const signatures: ConfirmedSignatureInfo[] = await connection.getSignaturesForAddress(address, { + before: currSignature, + until: toSignature, + limit, + }); + const txs = await connection.getParsedTransactions( + signatures.map((s) => s.signature), + { + maxSupportedTransactionVersion: 0, + } + ); + if (txs.length !== signatures.length) { + throw new Error(`failed to fetch tx for signatures`); + } + results.push(...txs); + numSignatures = signatures.length; + currSignature = signatures[signatures.length - 1].signature; + } while (numSignatures === limit); + + return results; +} diff --git a/cloud_functions/src/index.ts b/cloud_functions/src/index.ts index 82951ec0..e42b7565 100644 --- a/cloud_functions/src/index.ts +++ b/cloud_functions/src/index.ts @@ -24,6 +24,7 @@ export const { updateTokenMetadata } = require('./updateTokenMetadata'); export const { getReobserveVaas } = require('./getReobserveVaas'); export const { wormchainMonitor } = require('./wormchainMonitor'); export const { getLatestTokenData } = require('./getLatestTokenData'); +export const { getSolanaEvents } = require('./getSolanaEvents'); // Register an HTTP function with the Functions Framework that will be executed // when you make an HTTP request to the deployed function's endpoint. @@ -49,3 +50,4 @@ functions.http('updateTokenMetadata', updateTokenMetadata); functions.http('getReobserveVaas', getReobserveVaas); functions.http('wormchainMonitor', wormchainMonitor); functions.http('latestTokenData', getLatestTokenData); +functions.http('getSolanaEvents', getSolanaEvents); diff --git a/package-lock.json b/package-lock.json index 3dece2ee..1f7d8224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,9 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", + "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", + "borsh": "^1.0.0", "dotenv": "^16.0.3", "firebase-admin": "^11.4.1", "knex": "^2.4.2", @@ -207,6 +209,11 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" }, + "cloud_functions/node_modules/borsh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", + "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==" + }, "cloud_functions/node_modules/cosmjs-types": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.8.0.tgz", @@ -2099,15 +2106,21 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "license": "MIT", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/@babel/template": { "version": "7.20.7", "license": "MIT", @@ -6131,34 +6144,6 @@ "@scure/base": "~1.1.0" } }, - "node_modules/@mysten/sui.js/node_modules/@types/node": { - "version": "12.20.55", - "license": "MIT" - }, - "node_modules/@mysten/sui.js/node_modules/jayson": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "@types/connect": "^3.4.33", - "@types/node": "^12.12.54", - "@types/ws": "^7.4.4", - "commander": "^2.20.3", - "delay": "^5.0.0", - "es6-promisify": "^5.0.0", - "eyes": "^0.1.8", - "isomorphic-ws": "^4.0.1", - "json-stringify-safe": "^5.0.1", - "JSONStream": "^1.3.5", - "uuid": "^8.3.2", - "ws": "^7.4.5" - }, - "bin": { - "jayson": "bin/jayson.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@mysten/sui.js/node_modules/superstruct": { "version": "1.0.3", "license": "MIT", @@ -6409,16 +6394,6 @@ ], "license": "MIT" }, - "node_modules/@noble/ed25519": { - "version": "1.7.1", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, "node_modules/@noble/hashes": { "version": "1.1.5", "funding": [ @@ -6429,16 +6404,6 @@ ], "license": "MIT" }, - "node_modules/@noble/secp256k1": { - "version": "1.7.0", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -6863,50 +6828,66 @@ } }, "node_modules/@solana/web3.js": { - "version": "1.73.0", - "license": "MIT", + "version": "1.87.3", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.87.3.tgz", + "integrity": "sha512-WGLzTZpi00vP443qGK3gL+LZXQJwaWkh6bzNXYpMTCAH2Z102y3YbPWOoQzJUeRSZWSXKh7MFkA3vDMFlMvGZQ==", "dependencies": { - "@babel/runtime": "^7.12.5", - "@noble/ed25519": "^1.7.0", - "@noble/hashes": "^1.1.2", - "@noble/secp256k1": "^1.6.3", + "@babel/runtime": "^7.23.2", + "@noble/curves": "^1.2.0", + "@noble/hashes": "^1.3.1", "@solana/buffer-layout": "^4.0.0", - "agentkeepalive": "^4.2.1", + "agentkeepalive": "^4.3.0", "bigint-buffer": "^1.1.5", - "bn.js": "^5.0.0", + "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", - "buffer": "6.0.1", + "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", - "jayson": "^3.4.4", - "node-fetch": "2", - "rpc-websockets": "^7.5.0", + "jayson": "^4.1.0", + "node-fetch": "^2.6.12", + "rpc-websockets": "^7.5.1", "superstruct": "^0.14.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@solana/web3.js/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "engines": { - "node": ">=12.20.0" + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@solana/web3.js/node_modules/buffer": { - "version": "6.0.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", + "node_modules/@solana/web3.js/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/@solana/web3.js/node_modules/superstruct": { @@ -8602,11 +8583,10 @@ } }, "node_modules/agentkeepalive": { - "version": "4.2.1", - "license": "MIT", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" }, "engines": { @@ -14377,7 +14357,8 @@ }, "node_modules/humanize-ms": { "version": "1.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "dependencies": { "ms": "^2.0.0" } @@ -15149,8 +15130,9 @@ } }, "node_modules/jayson": { - "version": "3.7.0", - "license": "MIT", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", @@ -15162,7 +15144,6 @@ "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "JSONStream": "^1.3.5", - "lodash": "^4.17.20", "uuid": "^8.3.2", "ws": "^7.4.5" }, @@ -15175,7 +15156,8 @@ }, "node_modules/jayson/node_modules/@types/node": { "version": "12.20.55", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/jest": { "version": "27.5.1", @@ -26894,7 +26876,8 @@ "watcher/node_modules/@types/node": { "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true }, "watcher/node_modules/ansi-styles": { "version": "4.3.0", @@ -27103,31 +27086,6 @@ "version": "0.3.2", "license": "MIT" }, - "watcher/node_modules/jayson": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", - "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", - "dependencies": { - "@types/connect": "^3.4.33", - "@types/node": "^12.12.54", - "@types/ws": "^7.4.4", - "commander": "^2.20.3", - "delay": "^5.0.0", - "es6-promisify": "^5.0.0", - "eyes": "^0.1.8", - "isomorphic-ws": "^4.0.1", - "json-stringify-safe": "^5.0.1", - "JSONStream": "^1.3.5", - "uuid": "^8.3.2", - "ws": "^7.4.5" - }, - "bin": { - "jayson": "bin/jayson.js" - }, - "engines": { - "node": ">=8" - } - }, "watcher/node_modules/jest": { "version": "29.3.1", "dev": true, @@ -29071,9 +29029,18 @@ "version": "0.8.0" }, "@babel/runtime": { - "version": "7.21.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + } } }, "@babel/template": { @@ -31684,26 +31651,6 @@ "@scure/base": "~1.1.0" } }, - "@types/node": { - "version": "12.20.55" - }, - "jayson": { - "version": "4.1.0", - "requires": { - "@types/connect": "^3.4.33", - "@types/node": "^12.12.54", - "@types/ws": "^7.4.4", - "commander": "^2.20.3", - "delay": "^5.0.0", - "es6-promisify": "^5.0.0", - "eyes": "^0.1.8", - "isomorphic-ws": "^4.0.1", - "json-stringify-safe": "^5.0.1", - "JSONStream": "^1.3.5", - "uuid": "^8.3.2", - "ws": "^7.4.5" - } - }, "superstruct": { "version": "1.0.3" } @@ -31816,15 +31763,9 @@ } } }, - "@noble/ed25519": { - "version": "1.7.1" - }, "@noble/hashes": { "version": "1.1.5" }, - "@noble/secp256k1": { - "version": "1.7.0" - }, "@nodelib/fs.scandir": { "version": "2.1.5", "requires": { @@ -32059,31 +32000,46 @@ } }, "@solana/web3.js": { - "version": "1.73.0", + "version": "1.87.3", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.87.3.tgz", + "integrity": "sha512-WGLzTZpi00vP443qGK3gL+LZXQJwaWkh6bzNXYpMTCAH2Z102y3YbPWOoQzJUeRSZWSXKh7MFkA3vDMFlMvGZQ==", "requires": { - "@babel/runtime": "^7.12.5", - "@noble/ed25519": "^1.7.0", - "@noble/hashes": "^1.1.2", - "@noble/secp256k1": "^1.6.3", + "@babel/runtime": "^7.23.2", + "@noble/curves": "^1.2.0", + "@noble/hashes": "^1.3.1", "@solana/buffer-layout": "^4.0.0", - "agentkeepalive": "^4.2.1", + "agentkeepalive": "^4.3.0", "bigint-buffer": "^1.1.5", - "bn.js": "^5.0.0", + "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", - "buffer": "6.0.1", + "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", - "jayson": "^3.4.4", - "node-fetch": "2", - "rpc-websockets": "^7.5.0", + "jayson": "^4.1.0", + "node-fetch": "^2.6.12", + "rpc-websockets": "^7.5.1", "superstruct": "^0.14.2" }, "dependencies": { - "buffer": { - "version": "6.0.1", + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "@noble/hashes": "1.3.2" + } + }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" } }, "superstruct": { @@ -33178,7 +33134,9 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", + "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", + "borsh": "^1.0.0", "dotenv": "^16.0.3", "firebase-admin": "^11.4.1", "knex": "^2.4.2", @@ -33355,6 +33313,11 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" }, + "borsh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", + "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==" + }, "cosmjs-types": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.8.0.tgz", @@ -33768,7 +33731,8 @@ "@types/node": { "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true }, "ansi-styles": { "version": "4.3.0", @@ -33911,25 +33875,6 @@ "is-arrayish": { "version": "0.3.2" }, - "jayson": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", - "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", - "requires": { - "@types/connect": "^3.4.33", - "@types/node": "^12.12.54", - "@types/ws": "^7.4.4", - "commander": "^2.20.3", - "delay": "^5.0.0", - "es6-promisify": "^5.0.0", - "eyes": "^0.1.8", - "isomorphic-ws": "^4.0.1", - "json-stringify-safe": "^5.0.1", - "JSONStream": "^1.3.5", - "uuid": "^8.3.2", - "ws": "^7.4.5" - } - }, "jest": { "version": "29.3.1", "dev": true, @@ -34723,10 +34668,10 @@ } }, "agentkeepalive": { - "version": "4.2.1", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "requires": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" } }, @@ -38437,6 +38382,8 @@ }, "humanize-ms": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "requires": { "ms": "^2.0.0" } @@ -38842,7 +38789,9 @@ } }, "jayson": { - "version": "3.7.0", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", "requires": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", @@ -38854,13 +38803,14 @@ "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "JSONStream": "^1.3.5", - "lodash": "^4.17.20", "uuid": "^8.3.2", "ws": "^7.4.5" }, "dependencies": { "@types/node": { - "version": "12.20.55" + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" } } }, From 0100c61b95090768550227d986afb09cb56dc0ac Mon Sep 17 00:00:00 2001 From: Kevin Peters <kevin@w7.xyz> Date: Wed, 1 Nov 2023 12:04:25 -0500 Subject: [PATCH 2/3] cloud-functions: Added getSuiEvents cloud function --- cloud_functions/package.json | 6 +- cloud_functions/scripts/deploy.sh | 1 + cloud_functions/src/getSolanaEvents.ts | 11 +- cloud_functions/src/getSuiEvents.ts | 279 +++++++++++++++++++ cloud_functions/src/index.ts | 2 + cloud_functions/src/types.ts | 10 + cloud_functions/tsconfig.json | 3 +- package-lock.json | 353 ++++++++++++++++++++----- 8 files changed, 580 insertions(+), 85 deletions(-) create mode 100644 cloud_functions/src/getSuiEvents.ts diff --git a/cloud_functions/package.json b/cloud_functions/package.json index 3752895f..b55dd95b 100644 --- a/cloud_functions/package.json +++ b/cloud_functions/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "dev": "ts-node src/index.ts", - "start": "npx functions-framework --target=getSolanaEvents [--signature-type=http]", + "start": "npx functions-framework --target=getSuiEvents [--signature-type=http]", "deploy": "bash scripts/deploy.sh", "gcp-build": "npm i ./dist/src/wormhole-foundation-wormhole-monitor-common-0.0.1.tgz ./dist/src/wormhole-foundation-wormhole-monitor-database-0.0.1.tgz" }, @@ -18,6 +18,7 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", + "@mysten/sui.js": "^0.45.0", "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", "borsh": "^1.0.0", @@ -26,5 +27,8 @@ "knex": "^2.4.2", "path-to-regexp": "^6.2.1", "pg": "^8.10.0" + }, + "devDependencies": { + "typescript": "^5.2.2" } } diff --git a/cloud_functions/scripts/deploy.sh b/cloud_functions/scripts/deploy.sh index aab28384..0aecac8f 100755 --- a/cloud_functions/scripts/deploy.sh +++ b/cloud_functions/scripts/deploy.sh @@ -226,6 +226,7 @@ gcloud functions --project "$GCP_PROJECT" deploy refresh-todays-token-prices --e gcloud functions --project "$GCP_PROJECT" deploy update-token-metadata --entry-point updateTokenMetadata --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE gcloud functions --project "$GCP_PROJECT" deploy wormchain-monitor --entry-point wormchainMonitor --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars WORMCHAIN_SLACK_CHANNEL_ID=$WORMCHAIN_SLACK_CHANNEL_ID,WORMCHAIN_SLACK_POST_URL=$WORMCHAIN_SLACK_POST_URL,WORMCHAIN_SLACK_BOT_TOKEN=$WORMCHAIN_SLACK_BOT_TOKEN,WORMCHAIN_PAGERDUTY_ROUTING_KEY=$WORMCHAIN_PAGERDUTY_ROUTING_KEY,WORMCHAIN_PAGERDUTY_URL=$WORMCHAIN_PAGERDUTY_URL gcloud functions --project "$GCP_PROJECT" deploy get-solana-events --entry-point getSolanaEvents --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars SOLANA_RPC=$SOLANA_RPC +gcloud functions --project "$GCP_PROJECT" deploy get-sui-events --entry-point getSuiEvents --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 if [ "$NETWORK" == "MAINNET" ]; then echo "Finished deploying MAINNET functions" diff --git a/cloud_functions/src/getSolanaEvents.ts b/cloud_functions/src/getSolanaEvents.ts index bac1fda9..fe4db2d2 100644 --- a/cloud_functions/src/getSolanaEvents.ts +++ b/cloud_functions/src/getSolanaEvents.ts @@ -12,16 +12,7 @@ import * as ethers from 'ethers'; import * as bs58 from 'bs58'; import { deserialize } from 'borsh'; import { assertEnvironmentVariable } from '@wormhole-foundation/wormhole-monitor-common'; - -interface EventData { - blockNumber: number; - txHash: string; - from: string; - to: string; - token: string; - amount: string; - isDeposit: boolean; -} +import { EventData } from './types'; export async function getSolanaEvents(req: any, res: any) { res.set('Access-Control-Allow-Origin', '*'); diff --git a/cloud_functions/src/getSuiEvents.ts b/cloud_functions/src/getSuiEvents.ts new file mode 100644 index 00000000..1ea7c63b --- /dev/null +++ b/cloud_functions/src/getSuiEvents.ts @@ -0,0 +1,279 @@ +import { ethers } from 'ethers'; +import { + SuiClient, + SuiEvent, + SuiObjectChange, + SuiTransactionBlockResponse, + getFullnodeUrl, + PaginatedTransactionResponse, +} from '@mysten/sui.js/client'; +import { normalizeSuiAddress, SUI_TYPE_ARG } from '@mysten/sui.js/utils'; +import { EventData } from './types'; + +const wormholeMessageEventType = + '0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a::publish_message::WormholeMessage'; +const tokenBridgeAddress = '0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9'; +const originalTokenBridgePackageId = + '0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8eba0697695e3d'; + +export async function getSuiEvents(req: any, res: any) { + res.set('Access-Control-Allow-Origin', '*'); + if (req.method === 'OPTIONS') { + // Send response to OPTIONS requests + res.set('Access-Control-Allow-Methods', 'GET'); + res.set('Access-Control-Allow-Headers', 'Content-Type'); + res.set('Access-Control-Max-Age', '3600'); + res.sendStatus(204); + return; + } + if (!req.query.fromCheckpoint) { + res.status(400).send('fromCheckpoint is required'); + return; + } + if (!req.query.toCheckpoint) { + res.status(400).send('toCheckpoint is required'); + return; + } + try { + const fromCheckpoint = Number(req.query.fromCheckpoint); + const toCheckpoint = Number(req.query.toCheckpoint); + console.log(`fetching events from ${fromCheckpoint} to ${toCheckpoint}`); + const events = await _getSuiEvents(fromCheckpoint, toCheckpoint); + console.log(`fetched ${events.length} events`); + res.json(events); + } catch (e) { + console.error(e); + res.sendStatus(500); + } +} + +/** + * Retrieves Sui events from a given checkpoint range using the token bridge. + * Optimized to make as few RPC calls as possible. + * @param fromCheckpoint The starting checkpoint to retrieve events from. + * @param toCheckpoint The ending checkpoint to retrieve events from. + * @returns An array of EventData objects representing the events that occurred within the given checkpoint range. + */ +const _getSuiEvents = async ( + fromCheckpoint: number, + toCheckpoint: number +): Promise<EventData[]> => { + const events: EventData[] = []; + const txBlocks = await getTransactionBlocks(fromCheckpoint, toCheckpoint, tokenBridgeAddress); + for (const txBlock of txBlocks) { + if ( + txBlock.effects?.status.status !== 'success' || + !txBlock.checkpoint || + !txBlock.objectChanges || + txBlock.transaction?.data.transaction.kind !== 'ProgrammableTransaction' + ) { + continue; + } + const transactions = txBlock.transaction.data.transaction.transactions; + for (const tx of transactions) { + const moveCall = 'MoveCall' in tx && tx.MoveCall; + if (!moveCall || moveCall.package !== originalTokenBridgePackageId) { + continue; + } + if ( + (moveCall.module === 'complete_transfer_with_payload' && + moveCall.function === 'authorize_transfer') || + (moveCall.module === 'complete_transfer' && moveCall.function === 'authorize_transfer') + ) { + const token = moveCall.type_arguments![0]; + // search backwards for the parse_and_verify call + const parseAndVerifyTx = transactions + .slice( + 0, + transactions.findIndex((value) => value === tx) + ) + .reverse() + .find( + (tx) => + 'MoveCall' in tx && + tx.MoveCall.module === 'vaa' && + tx.MoveCall.function === 'parse_and_verify' + ); + if (!parseAndVerifyTx || !('MoveCall' in parseAndVerifyTx)) { + continue; + } + const vaaArg = parseAndVerifyTx.MoveCall.arguments?.[1]; + if (!vaaArg || typeof vaaArg !== 'object' || !('Input' in vaaArg)) { + continue; + } + const vaaInput = txBlock.transaction.data.transaction.inputs[vaaArg.Input]; + if (!vaaInput || vaaInput.type !== 'pure' || vaaInput.valueType !== 'vector<u8>') { + continue; + } + const vaa = Buffer.from(vaaInput.value as number[]); + const sigStart = 6; + const numSigners = vaa[5]; + const sigLength = 66; + const body = vaa.subarray(sigStart + sigLength * numSigners); + const payload = body.subarray(51); + const type = payload.readUInt8(0); + if (type !== 1 && type !== 3) { + continue; + } + const amount = await denormalizeAmount( + token, + ethers.BigNumber.from(payload.subarray(1, 33)) + ); + const to = `0x${payload.subarray(67, 99).toString('hex')}`; + const event: EventData = { + blockNumber: Number(txBlock.checkpoint), + txHash: txBlock.digest, + // Wrapped tokens are minted from the zero address on Ethereum + // Override the from address to be the zero address for consistency + from: isWrappedToken(token, txBlock.objectChanges) + ? ethers.constants.AddressZero + : tokenBridgeAddress, + to, + token, + amount: amount.toString(), + isDeposit: false, + }; + events.push(event); + } + if ( + ((moveCall.module === 'transfer_tokens_with_payload' && + moveCall.function === 'transfer_tokens_with_payload') || + (moveCall.module === 'transfer_tokens' && moveCall.function === 'transfer_tokens')) && + txBlock.events + ) { + const token = tx.MoveCall.type_arguments![0]; + const payload = getWormholeMessagePayload(txBlock.events); + const originChain = payload.readUint16BE(65); + const toChain = payload.readUInt16BE(99); + const amount = await denormalizeAmount( + token, + ethers.BigNumber.from(payload.subarray(1, 33)) + ); + const isWrapped = isWrappedToken(token, txBlock.objectChanges); + const event: EventData = { + blockNumber: Number(txBlock.checkpoint), + txHash: txBlock.digest, + from: txBlock.transaction.data.sender, + // if this is a wrapped token being burned and not being sent to its origin chain, + // then it should be included in the volume by fixing the to address + to: + !isWrapped || originChain !== toChain + ? tokenBridgeAddress + : ethers.constants.AddressZero, + token, + amount: amount.toString(), + isDeposit: !isWrapped, + }; + events.push(event); + } + } + } + return events; +}; + +const getWormholeMessagePayload = (events: SuiEvent[]): Buffer => { + const filtered = events.filter((event) => { + return event.type === wormholeMessageEventType; + }); + // TODO: support multiple transfers in a single txBlock + if (filtered.length !== 1) { + throw new Error(`Expected exactly one wormhole message event, found ${filtered.length}`); + } + return Buffer.from((filtered[0].parsedJson as any).payload); +}; + +const tokenDecimalsCache: { [token: string]: number } = {}; + +const getTokenDecimals = async (token: string): Promise<number> => { + if (token in tokenDecimalsCache) { + return tokenDecimalsCache[token]; + } + const client = getClient(); + const coinMetadata = await client.getCoinMetadata({ coinType: token }); + if (coinMetadata === null) { + throw new Error(`Failed to get coin metadata for ${token}`); + } + const { decimals } = coinMetadata; + tokenDecimalsCache[token] = decimals; + return decimals; +}; + +const denormalizeAmount = async ( + token: string, + amount: ethers.BigNumber +): Promise<ethers.BigNumber> => { + const decimals = await getTokenDecimals(token); + if (decimals > 8) { + return amount.mul(ethers.BigNumber.from(10).pow(decimals - 8)); + } + return amount; +}; + +const isWrappedToken = (token: string, objectChanges: SuiObjectChange[]) => { + const split = token.split('::'); + if (split.length !== 3) { + throw new Error(`Invalid token ${token}`); + } + const normalized = + token === SUI_TYPE_ARG ? token : `${normalizeSuiAddress(split[0])}::${split[1]}::${split[2]}`; + const nativeKey = `0x2::dynamic_field::Field<${originalTokenBridgePackageId}::token_registry::Key<${normalized}>, ${originalTokenBridgePackageId}::native_asset::NativeAsset<${normalized}>>`; + const wrappedKey = `0x2::dynamic_field::Field<${originalTokenBridgePackageId}::token_registry::Key<${normalized}>, ${originalTokenBridgePackageId}::wrapped_asset::WrappedAsset<${normalized}>>`; + const value = objectChanges.find( + (change) => change.type === 'mutated' && [nativeKey, wrappedKey].includes(change.objectType) + ); + if (!value) { + throw new Error(`Failed to find object change for token ${normalized}`); + } + return value.type === 'mutated' && value.objectType === wrappedKey; +}; + +export const getTransactionBlocks = async ( + fromCheckpoint: number, + toCheckpoint: number, + changedObject: string +): Promise<SuiTransactionBlockResponse[]> => { + const client = getClient(); + const results: SuiTransactionBlockResponse[] = []; + let hasNextPage = false; + let cursor: string | null | undefined = undefined; + let oldestCheckpoint: string | null = null; + do { + // TODO: The public RPC doesn't support fetching events by chaining filters with a `TimeRange` filter, + // so we have to search backwards for our checkpoint range + const response: PaginatedTransactionResponse = await client.queryTransactionBlocks({ + filter: { ChangedObject: changedObject }, + cursor, + options: { + showEffects: true, + showEvents: true, + showInput: true, + showObjectChanges: true, + }, + }); + for (const txBlock of response.data) { + const checkpoint = txBlock.checkpoint; + if (!checkpoint) { + continue; + } + if (checkpoint >= fromCheckpoint.toString() && checkpoint <= toCheckpoint.toString()) { + results.push(txBlock); + } + if (oldestCheckpoint === null || checkpoint < oldestCheckpoint) { + oldestCheckpoint = checkpoint; + } + } + hasNextPage = response.hasNextPage; + cursor = response.nextCursor; + } while ( + hasNextPage && + cursor && + oldestCheckpoint && + oldestCheckpoint >= fromCheckpoint.toString() + ); + return results; +}; + +const getClient = () => { + const url = process.env.SUI_RPC ?? getFullnodeUrl('mainnet'); + return new SuiClient({ url }); +}; diff --git a/cloud_functions/src/index.ts b/cloud_functions/src/index.ts index e42b7565..f0d5dd72 100644 --- a/cloud_functions/src/index.ts +++ b/cloud_functions/src/index.ts @@ -25,6 +25,7 @@ export const { getReobserveVaas } = require('./getReobserveVaas'); export const { wormchainMonitor } = require('./wormchainMonitor'); export const { getLatestTokenData } = require('./getLatestTokenData'); export const { getSolanaEvents } = require('./getSolanaEvents'); +export const { getSuiEvents } = require('./getSuiEvents'); // Register an HTTP function with the Functions Framework that will be executed // when you make an HTTP request to the deployed function's endpoint. @@ -51,3 +52,4 @@ functions.http('getReobserveVaas', getReobserveVaas); functions.http('wormchainMonitor', wormchainMonitor); functions.http('latestTokenData', getLatestTokenData); functions.http('getSolanaEvents', getSolanaEvents); +functions.http('getSuiEvents', getSuiEvents); diff --git a/cloud_functions/src/types.ts b/cloud_functions/src/types.ts index 5871455b..43d846fe 100644 --- a/cloud_functions/src/types.ts +++ b/cloud_functions/src/types.ts @@ -378,3 +378,13 @@ export type SlackInfo = { msg: string; bannerTxt: string; }; + +export interface EventData { + blockNumber: number; + txHash: string; + from: string; + to: string; + token: string; + amount: string; + isDeposit: boolean; +} diff --git a/cloud_functions/tsconfig.json b/cloud_functions/tsconfig.json index 7b1a4db1..f179f543 100644 --- a/cloud_functions/tsconfig.json +++ b/cloud_functions/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig.base.json", "references": [{ "path": "../common" }, { "path": "../database" }], "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src", "src/data/*.json"] } diff --git a/package-lock.json b/package-lock.json index 1f7d8224..a1e5fc95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", + "@mysten/sui.js": "^0.45.0", "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", "borsh": "^1.0.0", @@ -40,6 +41,9 @@ "knex": "^2.4.2", "path-to-regexp": "^6.2.1", "pg": "^8.10.0" + }, + "devDependencies": { + "typescript": "^5.2.2" } }, "cloud_functions/node_modules/@cosmjs/amino": { @@ -194,6 +198,57 @@ "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.31.1.tgz", "integrity": "sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==" }, + "cloud_functions/node_modules/@mysten/bcs": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.8.1.tgz", + "integrity": "sha512-wSEdP7QEfGQdb34g+7R0f3OdRqrv88iIABfJVDVJ6IsGLYVILreh8dZfNpZNUUyzctiyhX7zB9e/lR5qkddFPA==", + "dependencies": { + "bs58": "^5.0.0" + } + }, + "cloud_functions/node_modules/@mysten/sui.js": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.45.0.tgz", + "integrity": "sha512-t+LyGWzKTH+TM3c7apvK2aohnx6hj0nxjqrqbk49X/uLxhvDKEIoTGJDQC2cfDTOg6OteYTefCNsfRauh8wAzA==", + "dependencies": { + "@mysten/bcs": "0.8.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@open-rpc/client-js": "^1.8.1", + "@scure/bip32": "^1.3.1", + "@scure/bip39": "^1.2.1", + "@suchipi/femver": "^1.0.0", + "events": "^3.3.0", + "superstruct": "^1.0.3", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "cloud_functions/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "cloud_functions/node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "cloud_functions/node_modules/axios": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", @@ -204,6 +259,11 @@ "proxy-from-env": "^1.1.0" } }, + "cloud_functions/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, "cloud_functions/node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -214,6 +274,14 @@ "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==" }, + "cloud_functions/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dependencies": { + "base-x": "^4.0.0" + } + }, "cloud_functions/node_modules/cosmjs-types": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.8.0.tgz", @@ -248,6 +316,27 @@ "pbts": "bin/pbts" } }, + "cloud_functions/node_modules/superstruct": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", + "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==", + "engines": { + "node": ">=14.0.0" + } + }, + "cloud_functions/node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "common": { "name": "@wormhole-foundation/wormhole-monitor-common", "version": "0.0.1" @@ -6372,27 +6461,26 @@ } }, "node_modules/@noble/curves": { - "version": "1.0.0", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dependencies": { - "@noble/hashes": "1.3.0" + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.0", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@noble/hashes": { "version": "1.1.5", @@ -6433,6 +6521,25 @@ "node": ">= 8" } }, + "node_modules/@open-rpc/client-js": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz", + "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==", + "dependencies": { + "isomorphic-fetch": "^3.0.0", + "isomorphic-ws": "^5.0.0", + "strict-event-emitter-types": "^2.0.0", + "ws": "^7.0.0" + } + }, + "node_modules/@open-rpc/client-js/node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/@opentelemetry/api": { "version": "1.4.1", "license": "Apache-2.0", @@ -6713,39 +6820,36 @@ "license": "MIT" }, "node_modules/@scure/base": { - "version": "1.1.1", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", + "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@scure/bip32": { - "version": "1.3.0", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", "dependencies": { - "@noble/curves": "~1.0.0", - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.3.0", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@scure/bip39": { "version": "1.1.0", @@ -6849,17 +6953,6 @@ "superstruct": "^0.14.2" } }, - "node_modules/@solana/web3.js/node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@solana/web3.js/node_modules/@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", @@ -14964,6 +15057,15 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/isomorphic-ws": { "version": "4.0.1", "license": "MIT", @@ -23632,6 +23734,11 @@ "version": "1.0.1", "license": "MIT" }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" + }, "node_modules/strict-uri-encode": { "version": "1.1.0", "license": "MIT", @@ -31753,13 +31860,17 @@ } }, "@noble/curves": { - "version": "1.0.0", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "requires": { - "@noble/hashes": "1.3.0" + "@noble/hashes": "1.3.2" }, "dependencies": { "@noble/hashes": { - "version": "1.3.0" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" } } }, @@ -31783,6 +31894,25 @@ "fastq": "^1.6.0" } }, + "@open-rpc/client-js": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz", + "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==", + "requires": { + "isomorphic-fetch": "^3.0.0", + "isomorphic-ws": "^5.0.0", + "strict-event-emitter-types": "^2.0.0", + "ws": "^7.0.0" + }, + "dependencies": { + "isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "requires": {} + } + } + }, "@opentelemetry/api": { "version": "1.4.1" }, @@ -31936,18 +32066,24 @@ "version": "1.2.0" }, "@scure/base": { - "version": "1.1.1" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", + "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==" }, "@scure/bip32": { - "version": "1.3.0", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", "requires": { - "@noble/curves": "~1.0.0", - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" }, "dependencies": { "@noble/hashes": { - "version": "1.3.0" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" } } }, @@ -32021,14 +32157,6 @@ "superstruct": "^0.14.2" }, "dependencies": { - "@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "requires": { - "@noble/hashes": "1.3.2" - } - }, "@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", @@ -33134,6 +33262,7 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", + "@mysten/sui.js": "^0.45.0", "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", "borsh": "^1.0.0", @@ -33141,7 +33270,8 @@ "firebase-admin": "^11.4.1", "knex": "^2.4.2", "path-to-regexp": "^6.2.1", - "pg": "^8.10.0" + "pg": "^8.10.0", + "typescript": "^5.2.2" }, "dependencies": { "@cosmjs/amino": { @@ -33298,6 +33428,45 @@ "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.31.1.tgz", "integrity": "sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==" }, + "@mysten/bcs": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.8.1.tgz", + "integrity": "sha512-wSEdP7QEfGQdb34g+7R0f3OdRqrv88iIABfJVDVJ6IsGLYVILreh8dZfNpZNUUyzctiyhX7zB9e/lR5qkddFPA==", + "requires": { + "bs58": "^5.0.0" + } + }, + "@mysten/sui.js": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.45.0.tgz", + "integrity": "sha512-t+LyGWzKTH+TM3c7apvK2aohnx6hj0nxjqrqbk49X/uLxhvDKEIoTGJDQC2cfDTOg6OteYTefCNsfRauh8wAzA==", + "requires": { + "@mysten/bcs": "0.8.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@open-rpc/client-js": "^1.8.1", + "@scure/bip32": "^1.3.1", + "@scure/bip39": "^1.2.1", + "@suchipi/femver": "^1.0.0", + "events": "^3.3.0", + "superstruct": "^1.0.3", + "tweetnacl": "^1.0.3" + } + }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "requires": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, "axios": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", @@ -33308,6 +33477,11 @@ "proxy-from-env": "^1.1.0" } }, + "base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, "bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -33318,6 +33492,14 @@ "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==" }, + "bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "requires": { + "base-x": "^4.0.0" + } + }, "cosmjs-types": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.8.0.tgz", @@ -33346,6 +33528,17 @@ "@types/node": ">=13.7.0", "long": "^4.0.0" } + }, + "superstruct": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", + "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==" + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true } } }, @@ -38687,6 +38880,15 @@ "isexe": { "version": "2.0.0" }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "isomorphic-ws": { "version": "4.0.1", "requires": {} @@ -43982,6 +44184,11 @@ "stream-shift": { "version": "1.0.1" }, + "strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" + }, "strict-uri-encode": { "version": "1.1.0" }, From 9744c2dc74752f240e958d733694906ed47afe30 Mon Sep 17 00:00:00 2001 From: Kevin Peters <kevin@wormholelabs.xyz> Date: Tue, 19 Mar 2024 18:35:32 -0500 Subject: [PATCH 3/3] cloud-functions: rm getSuiEvents (build issues) --- cloud_functions/package.json | 3 +- cloud_functions/scripts/deploy.sh | 1 - cloud_functions/src/getSuiEvents.ts | 279 ---------------------------- cloud_functions/src/index.ts | 2 - package-lock.json | 181 ------------------ 5 files changed, 1 insertion(+), 465 deletions(-) delete mode 100644 cloud_functions/src/getSuiEvents.ts diff --git a/cloud_functions/package.json b/cloud_functions/package.json index b55dd95b..58b71f06 100644 --- a/cloud_functions/package.json +++ b/cloud_functions/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "dev": "ts-node src/index.ts", - "start": "npx functions-framework --target=getSuiEvents [--signature-type=http]", + "start": "npx functions-framework --target=getSolanaEvents [--signature-type=http]", "deploy": "bash scripts/deploy.sh", "gcp-build": "npm i ./dist/src/wormhole-foundation-wormhole-monitor-common-0.0.1.tgz ./dist/src/wormhole-foundation-wormhole-monitor-database-0.0.1.tgz" }, @@ -18,7 +18,6 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", - "@mysten/sui.js": "^0.45.0", "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", "borsh": "^1.0.0", diff --git a/cloud_functions/scripts/deploy.sh b/cloud_functions/scripts/deploy.sh index 0aecac8f..aab28384 100755 --- a/cloud_functions/scripts/deploy.sh +++ b/cloud_functions/scripts/deploy.sh @@ -226,7 +226,6 @@ gcloud functions --project "$GCP_PROJECT" deploy refresh-todays-token-prices --e gcloud functions --project "$GCP_PROJECT" deploy update-token-metadata --entry-point updateTokenMetadata --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE gcloud functions --project "$GCP_PROJECT" deploy wormchain-monitor --entry-point wormchainMonitor --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars WORMCHAIN_SLACK_CHANNEL_ID=$WORMCHAIN_SLACK_CHANNEL_ID,WORMCHAIN_SLACK_POST_URL=$WORMCHAIN_SLACK_POST_URL,WORMCHAIN_SLACK_BOT_TOKEN=$WORMCHAIN_SLACK_BOT_TOKEN,WORMCHAIN_PAGERDUTY_ROUTING_KEY=$WORMCHAIN_PAGERDUTY_ROUTING_KEY,WORMCHAIN_PAGERDUTY_URL=$WORMCHAIN_PAGERDUTY_URL gcloud functions --project "$GCP_PROJECT" deploy get-solana-events --entry-point getSolanaEvents --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars SOLANA_RPC=$SOLANA_RPC -gcloud functions --project "$GCP_PROJECT" deploy get-sui-events --entry-point getSuiEvents --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 if [ "$NETWORK" == "MAINNET" ]; then echo "Finished deploying MAINNET functions" diff --git a/cloud_functions/src/getSuiEvents.ts b/cloud_functions/src/getSuiEvents.ts deleted file mode 100644 index 1ea7c63b..00000000 --- a/cloud_functions/src/getSuiEvents.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { ethers } from 'ethers'; -import { - SuiClient, - SuiEvent, - SuiObjectChange, - SuiTransactionBlockResponse, - getFullnodeUrl, - PaginatedTransactionResponse, -} from '@mysten/sui.js/client'; -import { normalizeSuiAddress, SUI_TYPE_ARG } from '@mysten/sui.js/utils'; -import { EventData } from './types'; - -const wormholeMessageEventType = - '0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a::publish_message::WormholeMessage'; -const tokenBridgeAddress = '0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9'; -const originalTokenBridgePackageId = - '0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8eba0697695e3d'; - -export async function getSuiEvents(req: any, res: any) { - res.set('Access-Control-Allow-Origin', '*'); - if (req.method === 'OPTIONS') { - // Send response to OPTIONS requests - res.set('Access-Control-Allow-Methods', 'GET'); - res.set('Access-Control-Allow-Headers', 'Content-Type'); - res.set('Access-Control-Max-Age', '3600'); - res.sendStatus(204); - return; - } - if (!req.query.fromCheckpoint) { - res.status(400).send('fromCheckpoint is required'); - return; - } - if (!req.query.toCheckpoint) { - res.status(400).send('toCheckpoint is required'); - return; - } - try { - const fromCheckpoint = Number(req.query.fromCheckpoint); - const toCheckpoint = Number(req.query.toCheckpoint); - console.log(`fetching events from ${fromCheckpoint} to ${toCheckpoint}`); - const events = await _getSuiEvents(fromCheckpoint, toCheckpoint); - console.log(`fetched ${events.length} events`); - res.json(events); - } catch (e) { - console.error(e); - res.sendStatus(500); - } -} - -/** - * Retrieves Sui events from a given checkpoint range using the token bridge. - * Optimized to make as few RPC calls as possible. - * @param fromCheckpoint The starting checkpoint to retrieve events from. - * @param toCheckpoint The ending checkpoint to retrieve events from. - * @returns An array of EventData objects representing the events that occurred within the given checkpoint range. - */ -const _getSuiEvents = async ( - fromCheckpoint: number, - toCheckpoint: number -): Promise<EventData[]> => { - const events: EventData[] = []; - const txBlocks = await getTransactionBlocks(fromCheckpoint, toCheckpoint, tokenBridgeAddress); - for (const txBlock of txBlocks) { - if ( - txBlock.effects?.status.status !== 'success' || - !txBlock.checkpoint || - !txBlock.objectChanges || - txBlock.transaction?.data.transaction.kind !== 'ProgrammableTransaction' - ) { - continue; - } - const transactions = txBlock.transaction.data.transaction.transactions; - for (const tx of transactions) { - const moveCall = 'MoveCall' in tx && tx.MoveCall; - if (!moveCall || moveCall.package !== originalTokenBridgePackageId) { - continue; - } - if ( - (moveCall.module === 'complete_transfer_with_payload' && - moveCall.function === 'authorize_transfer') || - (moveCall.module === 'complete_transfer' && moveCall.function === 'authorize_transfer') - ) { - const token = moveCall.type_arguments![0]; - // search backwards for the parse_and_verify call - const parseAndVerifyTx = transactions - .slice( - 0, - transactions.findIndex((value) => value === tx) - ) - .reverse() - .find( - (tx) => - 'MoveCall' in tx && - tx.MoveCall.module === 'vaa' && - tx.MoveCall.function === 'parse_and_verify' - ); - if (!parseAndVerifyTx || !('MoveCall' in parseAndVerifyTx)) { - continue; - } - const vaaArg = parseAndVerifyTx.MoveCall.arguments?.[1]; - if (!vaaArg || typeof vaaArg !== 'object' || !('Input' in vaaArg)) { - continue; - } - const vaaInput = txBlock.transaction.data.transaction.inputs[vaaArg.Input]; - if (!vaaInput || vaaInput.type !== 'pure' || vaaInput.valueType !== 'vector<u8>') { - continue; - } - const vaa = Buffer.from(vaaInput.value as number[]); - const sigStart = 6; - const numSigners = vaa[5]; - const sigLength = 66; - const body = vaa.subarray(sigStart + sigLength * numSigners); - const payload = body.subarray(51); - const type = payload.readUInt8(0); - if (type !== 1 && type !== 3) { - continue; - } - const amount = await denormalizeAmount( - token, - ethers.BigNumber.from(payload.subarray(1, 33)) - ); - const to = `0x${payload.subarray(67, 99).toString('hex')}`; - const event: EventData = { - blockNumber: Number(txBlock.checkpoint), - txHash: txBlock.digest, - // Wrapped tokens are minted from the zero address on Ethereum - // Override the from address to be the zero address for consistency - from: isWrappedToken(token, txBlock.objectChanges) - ? ethers.constants.AddressZero - : tokenBridgeAddress, - to, - token, - amount: amount.toString(), - isDeposit: false, - }; - events.push(event); - } - if ( - ((moveCall.module === 'transfer_tokens_with_payload' && - moveCall.function === 'transfer_tokens_with_payload') || - (moveCall.module === 'transfer_tokens' && moveCall.function === 'transfer_tokens')) && - txBlock.events - ) { - const token = tx.MoveCall.type_arguments![0]; - const payload = getWormholeMessagePayload(txBlock.events); - const originChain = payload.readUint16BE(65); - const toChain = payload.readUInt16BE(99); - const amount = await denormalizeAmount( - token, - ethers.BigNumber.from(payload.subarray(1, 33)) - ); - const isWrapped = isWrappedToken(token, txBlock.objectChanges); - const event: EventData = { - blockNumber: Number(txBlock.checkpoint), - txHash: txBlock.digest, - from: txBlock.transaction.data.sender, - // if this is a wrapped token being burned and not being sent to its origin chain, - // then it should be included in the volume by fixing the to address - to: - !isWrapped || originChain !== toChain - ? tokenBridgeAddress - : ethers.constants.AddressZero, - token, - amount: amount.toString(), - isDeposit: !isWrapped, - }; - events.push(event); - } - } - } - return events; -}; - -const getWormholeMessagePayload = (events: SuiEvent[]): Buffer => { - const filtered = events.filter((event) => { - return event.type === wormholeMessageEventType; - }); - // TODO: support multiple transfers in a single txBlock - if (filtered.length !== 1) { - throw new Error(`Expected exactly one wormhole message event, found ${filtered.length}`); - } - return Buffer.from((filtered[0].parsedJson as any).payload); -}; - -const tokenDecimalsCache: { [token: string]: number } = {}; - -const getTokenDecimals = async (token: string): Promise<number> => { - if (token in tokenDecimalsCache) { - return tokenDecimalsCache[token]; - } - const client = getClient(); - const coinMetadata = await client.getCoinMetadata({ coinType: token }); - if (coinMetadata === null) { - throw new Error(`Failed to get coin metadata for ${token}`); - } - const { decimals } = coinMetadata; - tokenDecimalsCache[token] = decimals; - return decimals; -}; - -const denormalizeAmount = async ( - token: string, - amount: ethers.BigNumber -): Promise<ethers.BigNumber> => { - const decimals = await getTokenDecimals(token); - if (decimals > 8) { - return amount.mul(ethers.BigNumber.from(10).pow(decimals - 8)); - } - return amount; -}; - -const isWrappedToken = (token: string, objectChanges: SuiObjectChange[]) => { - const split = token.split('::'); - if (split.length !== 3) { - throw new Error(`Invalid token ${token}`); - } - const normalized = - token === SUI_TYPE_ARG ? token : `${normalizeSuiAddress(split[0])}::${split[1]}::${split[2]}`; - const nativeKey = `0x2::dynamic_field::Field<${originalTokenBridgePackageId}::token_registry::Key<${normalized}>, ${originalTokenBridgePackageId}::native_asset::NativeAsset<${normalized}>>`; - const wrappedKey = `0x2::dynamic_field::Field<${originalTokenBridgePackageId}::token_registry::Key<${normalized}>, ${originalTokenBridgePackageId}::wrapped_asset::WrappedAsset<${normalized}>>`; - const value = objectChanges.find( - (change) => change.type === 'mutated' && [nativeKey, wrappedKey].includes(change.objectType) - ); - if (!value) { - throw new Error(`Failed to find object change for token ${normalized}`); - } - return value.type === 'mutated' && value.objectType === wrappedKey; -}; - -export const getTransactionBlocks = async ( - fromCheckpoint: number, - toCheckpoint: number, - changedObject: string -): Promise<SuiTransactionBlockResponse[]> => { - const client = getClient(); - const results: SuiTransactionBlockResponse[] = []; - let hasNextPage = false; - let cursor: string | null | undefined = undefined; - let oldestCheckpoint: string | null = null; - do { - // TODO: The public RPC doesn't support fetching events by chaining filters with a `TimeRange` filter, - // so we have to search backwards for our checkpoint range - const response: PaginatedTransactionResponse = await client.queryTransactionBlocks({ - filter: { ChangedObject: changedObject }, - cursor, - options: { - showEffects: true, - showEvents: true, - showInput: true, - showObjectChanges: true, - }, - }); - for (const txBlock of response.data) { - const checkpoint = txBlock.checkpoint; - if (!checkpoint) { - continue; - } - if (checkpoint >= fromCheckpoint.toString() && checkpoint <= toCheckpoint.toString()) { - results.push(txBlock); - } - if (oldestCheckpoint === null || checkpoint < oldestCheckpoint) { - oldestCheckpoint = checkpoint; - } - } - hasNextPage = response.hasNextPage; - cursor = response.nextCursor; - } while ( - hasNextPage && - cursor && - oldestCheckpoint && - oldestCheckpoint >= fromCheckpoint.toString() - ); - return results; -}; - -const getClient = () => { - const url = process.env.SUI_RPC ?? getFullnodeUrl('mainnet'); - return new SuiClient({ url }); -}; diff --git a/cloud_functions/src/index.ts b/cloud_functions/src/index.ts index f0d5dd72..e42b7565 100644 --- a/cloud_functions/src/index.ts +++ b/cloud_functions/src/index.ts @@ -25,7 +25,6 @@ export const { getReobserveVaas } = require('./getReobserveVaas'); export const { wormchainMonitor } = require('./wormchainMonitor'); export const { getLatestTokenData } = require('./getLatestTokenData'); export const { getSolanaEvents } = require('./getSolanaEvents'); -export const { getSuiEvents } = require('./getSuiEvents'); // Register an HTTP function with the Functions Framework that will be executed // when you make an HTTP request to the deployed function's endpoint. @@ -52,4 +51,3 @@ functions.http('getReobserveVaas', getReobserveVaas); functions.http('wormchainMonitor', wormchainMonitor); functions.http('latestTokenData', getLatestTokenData); functions.http('getSolanaEvents', getSolanaEvents); -functions.http('getSuiEvents', getSuiEvents); diff --git a/package-lock.json b/package-lock.json index a1e5fc95..86021fdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", - "@mysten/sui.js": "^0.45.0", "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", "borsh": "^1.0.0", @@ -198,34 +197,6 @@ "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.31.1.tgz", "integrity": "sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==" }, - "cloud_functions/node_modules/@mysten/bcs": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.8.1.tgz", - "integrity": "sha512-wSEdP7QEfGQdb34g+7R0f3OdRqrv88iIABfJVDVJ6IsGLYVILreh8dZfNpZNUUyzctiyhX7zB9e/lR5qkddFPA==", - "dependencies": { - "bs58": "^5.0.0" - } - }, - "cloud_functions/node_modules/@mysten/sui.js": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.45.0.tgz", - "integrity": "sha512-t+LyGWzKTH+TM3c7apvK2aohnx6hj0nxjqrqbk49X/uLxhvDKEIoTGJDQC2cfDTOg6OteYTefCNsfRauh8wAzA==", - "dependencies": { - "@mysten/bcs": "0.8.1", - "@noble/curves": "^1.1.0", - "@noble/hashes": "^1.3.1", - "@open-rpc/client-js": "^1.8.1", - "@scure/bip32": "^1.3.1", - "@scure/bip39": "^1.2.1", - "@suchipi/femver": "^1.0.0", - "events": "^3.3.0", - "superstruct": "^1.0.3", - "tweetnacl": "^1.0.3" - }, - "engines": { - "node": ">=16" - } - }, "cloud_functions/node_modules/@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", @@ -237,18 +208,6 @@ "url": "https://paulmillr.com/funding/" } }, - "cloud_functions/node_modules/@scure/bip39": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "dependencies": { - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "cloud_functions/node_modules/axios": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", @@ -259,11 +218,6 @@ "proxy-from-env": "^1.1.0" } }, - "cloud_functions/node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" - }, "cloud_functions/node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -274,14 +228,6 @@ "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==" }, - "cloud_functions/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "dependencies": { - "base-x": "^4.0.0" - } - }, "cloud_functions/node_modules/cosmjs-types": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.8.0.tgz", @@ -316,14 +262,6 @@ "pbts": "bin/pbts" } }, - "cloud_functions/node_modules/superstruct": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", - "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==", - "engines": { - "node": ">=14.0.0" - } - }, "cloud_functions/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -6521,25 +6459,6 @@ "node": ">= 8" } }, - "node_modules/@open-rpc/client-js": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz", - "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==", - "dependencies": { - "isomorphic-fetch": "^3.0.0", - "isomorphic-ws": "^5.0.0", - "strict-event-emitter-types": "^2.0.0", - "ws": "^7.0.0" - } - }, - "node_modules/@open-rpc/client-js/node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "peerDependencies": { - "ws": "*" - } - }, "node_modules/@opentelemetry/api": { "version": "1.4.1", "license": "Apache-2.0", @@ -15057,15 +14976,6 @@ "version": "2.0.0", "license": "ISC" }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, "node_modules/isomorphic-ws": { "version": "4.0.1", "license": "MIT", @@ -23734,11 +23644,6 @@ "version": "1.0.1", "license": "MIT" }, - "node_modules/strict-event-emitter-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", - "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" - }, "node_modules/strict-uri-encode": { "version": "1.1.0", "license": "MIT", @@ -31894,25 +31799,6 @@ "fastq": "^1.6.0" } }, - "@open-rpc/client-js": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz", - "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==", - "requires": { - "isomorphic-fetch": "^3.0.0", - "isomorphic-ws": "^5.0.0", - "strict-event-emitter-types": "^2.0.0", - "ws": "^7.0.0" - }, - "dependencies": { - "isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "requires": {} - } - } - }, "@opentelemetry/api": { "version": "1.4.1" }, @@ -33262,7 +33148,6 @@ "@google-cloud/functions-framework": "^3.1.3", "@google-cloud/pubsub": "^3.4.1", "@google-cloud/storage": "^6.8.0", - "@mysten/sui.js": "^0.45.0", "@solana/web3.js": "^1.87.3", "axios": "^1.5.0", "borsh": "^1.0.0", @@ -33428,45 +33313,11 @@ "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.31.1.tgz", "integrity": "sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==" }, - "@mysten/bcs": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.8.1.tgz", - "integrity": "sha512-wSEdP7QEfGQdb34g+7R0f3OdRqrv88iIABfJVDVJ6IsGLYVILreh8dZfNpZNUUyzctiyhX7zB9e/lR5qkddFPA==", - "requires": { - "bs58": "^5.0.0" - } - }, - "@mysten/sui.js": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.45.0.tgz", - "integrity": "sha512-t+LyGWzKTH+TM3c7apvK2aohnx6hj0nxjqrqbk49X/uLxhvDKEIoTGJDQC2cfDTOg6OteYTefCNsfRauh8wAzA==", - "requires": { - "@mysten/bcs": "0.8.1", - "@noble/curves": "^1.1.0", - "@noble/hashes": "^1.3.1", - "@open-rpc/client-js": "^1.8.1", - "@scure/bip32": "^1.3.1", - "@scure/bip39": "^1.2.1", - "@suchipi/femver": "^1.0.0", - "events": "^3.3.0", - "superstruct": "^1.0.3", - "tweetnacl": "^1.0.3" - } - }, "@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" }, - "@scure/bip39": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "requires": { - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" - } - }, "axios": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", @@ -33477,11 +33328,6 @@ "proxy-from-env": "^1.1.0" } }, - "base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" - }, "bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -33492,14 +33338,6 @@ "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==" }, - "bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "requires": { - "base-x": "^4.0.0" - } - }, "cosmjs-types": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.8.0.tgz", @@ -33529,11 +33367,6 @@ "long": "^4.0.0" } }, - "superstruct": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", - "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==" - }, "typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -38880,15 +38713,6 @@ "isexe": { "version": "2.0.0" }, - "isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "requires": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, "isomorphic-ws": { "version": "4.0.1", "requires": {} @@ -44184,11 +44008,6 @@ "stream-shift": { "version": "1.0.1" }, - "strict-event-emitter-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", - "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" - }, "strict-uri-encode": { "version": "1.1.0" },