Skip to content

Commit

Permalink
Merge pull request #612 from hop-protocol/feat/hn-cctp-updates
Browse files Browse the repository at this point in the history
Feat/hn cctp updates
  • Loading branch information
shanefontaine authored Jun 25, 2024
2 parents 8d2627a + dd3c535 commit 1916d2a
Show file tree
Hide file tree
Showing 46 changed files with 3,077 additions and 2,910 deletions.
3 changes: 2 additions & 1 deletion packages/hop-node/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ prometheus_pass.txt
*.txt
*.tsbuildinfo
cctp_db_data*
.npmignore
.npmignore
db_data*
1 change: 1 addition & 0 deletions packages/hop-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"ethereum-block-by-date": "1.4.9",
"ethers": "5.7.2",
"keythereum": "1.2.0",
"level": "8.0.1",
"level-party": "5.1.1",
"lodash": "4.17.21",
"luxon": "3.4.4",
Expand Down
185 changes: 23 additions & 162 deletions packages/hop-node/src/cctp/cctp/Message.ts
Original file line number Diff line number Diff line change
@@ -1,173 +1,34 @@
import type { Signer, providers} from 'ethers'
import { BigNumber, utils } from 'ethers'
import {
CCTP_DOMAIN_MAP,
getAttestationUrl,
getHopCCTPContract,
getMessageTransmitterContract
} from './utils.js'
import { ChainSlug, getChainSlug } from '@hop-protocol/sdk'
import type { NetworkSlug } from '@hop-protocol/sdk'
import { MIN_POLYGON_GAS_PRICE } from '#constants/index.js'
import type { RequiredEventFilter } from '../indexer/OnchainEventIndexer.js'
import { getRpcProvider } from '#utils/getRpcProvider.js'
import { config as globalConfig } from '#config/index.js'
import { Mutex } from 'async-mutex'
import { wait } from '#utils/wait.js'

// Temp to handle API rate limit
const mutex = new Mutex()

enum AttestationStatus {
PendingConfirmation = 'pending_confirmation',
Complete = 'complete'
}

interface IAttestationResponseError {
error: string
}

interface IAttestationResponseSuccess {
status: AttestationStatus
attestation: string
}

type IAttestationResponse = IAttestationResponseError | IAttestationResponseSuccess

// TODO: Get from SDK
export type HopCCTPTransferSentDecoded = {
amount: BigNumber
bonderFee: BigNumber
}

/**
* CCTP Message utility class. This class exposes all required chain interactions with CCTP
* contracts while being chain agnostic and stateless.
*/

// TODO: Sigs are redundant with the filters
import { MessageDataProvider } from './MessageDataProvider.js'
import { MessageIndexer } from './MessageIndexer.js'
import { MessageStateMachine } from './MessageStateMachine.js'
import { MessageState } from './types.js'

export class Message {
// TODO: Do this better and get from SDK
static HOP_CCTP_TRANSFER_SENT_SIG = '0x10bf4019e09db5876a05d237bfcc676cd84eee2c23f820284906dd7cfa70d2c4'
static MESSAGE_SENT_EVENT_SIG = '0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036'
static MESSAGE_RECEIVED_EVENT_SIG = '0x58200b4c34ae05ee816d710053fff3fb75af4395915d3d2a771b24aa10e3cc5d'

// TODO: Get from SDK
static getCCTPTransferSentEventFilter(chainId: number): RequiredEventFilter {
const contract = getHopCCTPContract(chainId)
return contract.filters.CCTPTransferSent() as RequiredEventFilter
}

// TODO: Get from SDK
static getMessageSentEventFilter(chainId: number): RequiredEventFilter {
const contract = getMessageTransmitterContract(chainId)
return contract.filters.MessageSent() as RequiredEventFilter
}

// TODO: Get from SDK
static getMessageReceivedEventFilter(chainId: number): RequiredEventFilter {
const contract = getMessageTransmitterContract(chainId)
return contract.filters.MessageReceived() as RequiredEventFilter
}
readonly #stateMachine: MessageStateMachine
#started: boolean = false

// TODO: better name, not just "event"
static decodeMessageFromEvent (encodedMessage: string): string {
const decoded = utils.defaultAbiCoder.decode(['bytes'], encodedMessage)
return decoded[0]
}

static getMessageHashFromMessage (message: string): string {
return utils.keccak256(message)
}

static decodeHopCCTPTransferSentFromEvent (data: string): HopCCTPTransferSentDecoded {
const res = utils.defaultAbiCoder.decode([
'uint256',
'uint256'
], data)

return {
amount: BigNumber.from(res[0]),
bonderFee: BigNumber.from(res[1])
}
}

// TODO: Get from SDK
static convertDomainToChainId (domainId: BigNumber): BigNumber {
const domainMap = CCTP_DOMAIN_MAP[globalConfig.network as NetworkSlug]
if (!domainMap) {
throw new Error('Domain map not found')
}
constructor (chainIds: string[]) {
const dbName = 'Message'
const states = [
MessageState.Sent,
MessageState.Relayed
]

return BigNumber.from(domainMap[Number(domainId)])
}
// Data handler
const indexer = new MessageIndexer(dbName, states, chainIds)
const dataProvider = new MessageDataProvider(indexer)

static async relayMessage (signer: Signer, message: string, attestation: string): Promise<providers.TransactionReceipt> {
const chainId = await signer.getChainId()
// Remove this in favor of the contract instance from the SDK when available
const MessageTransmitterContract = getMessageTransmitterContract(chainId)
// TODO: Config overrides
const txOverrides = await Message.getTxOverrides(chainId)
return MessageTransmitterContract.connect(signer).receiveMessage(message, attestation, txOverrides)
// State handler
this.#stateMachine = new MessageStateMachine(dbName, states, dataProvider)
}

/**
* Example API responses:
* {"error":"Message hash not found"}
* {"attestation":"PENDING","status":"pending_confirmations"}
* {"attestation":"0x123...","status":"complete"}
*/
static async fetchAttestation (messageHash: string): Promise<string> {
return await mutex.runExclusive(async () => {
const url = getAttestationUrl(messageHash)
console.log('temp000', messageHash)
const res = await fetch(url)
console.log('temp111', messageHash, res)
if (res.status === 429) {
// Temp to handle API rate limit
await wait(2_000)
}
const json: IAttestationResponse = await res.json()
console.log('temp222', messageHash, json)

if (!json) {
throw new Error('Message hash not found')
}

console.log('temp333', messageHash)
if ('error' in json) {
throw new Error(json.error)
}

console.log('temp444', messageHash)
if (json.status !== 'complete') {
throw new Error(`Attestation not complete: ${JSON.stringify(json)} (messageHash: ${messageHash})`)
}

console.log('temp555', messageHash, json)
return json.attestation
})
}

// TODO: rm for config
static async getTxOverrides (chainId: number): Promise<any>{
const chainSlug = getChainSlug(chainId.toString())
const provider = getRpcProvider(chainSlug)
const txOptions: any = {}

// Not all Polygon nodes follow recommended 30 Gwei gasPrice
// https://forum.matic.network/t/recommended-min-gas-price-setting/2531
if (chainSlug === ChainSlug.Polygon) {
txOptions.gasPrice = await provider.getGasPrice()

const minGasPrice = BigNumber.from(MIN_POLYGON_GAS_PRICE).mul(2)
const gasPriceBn = BigNumber.from(txOptions.gasPrice)
if (gasPriceBn.lt(minGasPrice)) {
txOptions.gasPrice = minGasPrice
}
async start (): Promise<void> {
if (this.#started) {
throw new Error('Already started')
}

return txOptions
await this.#stateMachine.init()
this.#stateMachine.start()
this.#started = true
}
}
91 changes: 91 additions & 0 deletions packages/hop-node/src/cctp/cctp/MessageDataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { DecodedLogWithContext } from '../types.js'
import {
MessageSDK,
type HopCCTPTransferSentDecodedWithMessage,
type HopCCTPTransferReceivedDecoded
} from './sdk/MessageSDK.js'
import { getRpcProvider } from '#utils/getRpcProvider.js'
import { DataProvider } from '../data-provider/DataProvider.js'
import { type IMessage, MessageState, type ISentMessage, type IRelayedMessage } from './types.js'

// Since the messages are unique by chainId, his MessageDataProvider should be the
// class that abstracts this away.

export class MessageDataProvider extends DataProvider<MessageState, IMessage> {

/**
* Implementation
*/

protected override getKeyFromDataSourceItem (log: DecodedLogWithContext): MessageState {
return this.#getStateFromLog(log)
}

protected override async formatDataSourceItem (state: MessageState, log: DecodedLogWithContext): Promise<IMessage> {
switch (state) {
case MessageState.Sent:
return this.#formatTransferSentLog(log as DecodedLogWithContext<HopCCTPTransferSentDecodedWithMessage>)
case MessageState.Relayed:
return this.#formatRelayedLog(log as DecodedLogWithContext<HopCCTPTransferReceivedDecoded>)
default:
throw new Error('Invalid state')
}
}

/**
* Internal
*/

async #formatTransferSentLog (log: DecodedLogWithContext<HopCCTPTransferSentDecodedWithMessage>): Promise<ISentMessage> {
const { transactionHash,context, decoded } = log
const { chainId } = context
const { message, cctpNonce, chainId: destinationChainId } = decoded
const timestampMs = await this.#getBlockTimestampFromLogMs(log)

return {
message,
messageNonce: cctpNonce,
sourceChainId: chainId,
destinationChainId,
sentTxHash: transactionHash,
sentTimestampMs: timestampMs
}
}

async #formatRelayedLog (log: DecodedLogWithContext<HopCCTPTransferReceivedDecoded>): Promise<IRelayedMessage> {
const { transactionHash, decoded, context } = log
const { nonce, sourceDomain } = decoded
const timestampMs = await this.#getBlockTimestampFromLogMs(log)
return {
messageNonce: nonce,
sourceChainId: MessageSDK.getChainIdFromDomain(sourceDomain),
destinationChainId: context.chainId,
relayTransactionHash: transactionHash,
relayTimestampMs: timestampMs
}
}

/**
* Utils
*/

async #getBlockTimestampFromLogMs (log: DecodedLogWithContext): Promise<number> {
const { context, blockNumber } = log
const provider = getRpcProvider(context.chainId)
const block = await provider.getBlock(blockNumber)
return block.timestamp * 1000
}

#getStateFromLog (log: DecodedLogWithContext): MessageState {
const eventSig = log.topics[0]
const chainId = log.context.chainId
switch (eventSig) {
case (MessageSDK.getCCTPTransferSentEventFilter(chainId).topics[0]):
return MessageState.Sent
case (MessageSDK.getMessageReceivedEventFilter(chainId).topics[0]):
return MessageState.Relayed
default:
throw new Error('Invalid log')
}
}
}
Loading

0 comments on commit 1916d2a

Please sign in to comment.