parent
98e09ae0e2
commit
2b63e14fcb
@ -1,5 +0,0 @@ |
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */ |
||||
module.exports = { |
||||
preset: 'ts-jest', |
||||
testEnvironment: 'node', |
||||
}; |
@ -1,7 +1,7 @@ |
||||
// This is the ABI for the ProofsService.
|
||||
// This is used to 1) Select the function 2) encode output
|
||||
const ProofsServiceAbi = [ |
||||
'function getProofs(address target, bytes32 storageKey, bytes32 slot) public view returns (string[][])', |
||||
'function getProofs(address target, bytes32 storageKey, uint256 slot) public view returns (string[][])', |
||||
]; |
||||
|
||||
export { ProofsServiceAbi }; |
||||
|
@ -1,7 +0,0 @@ |
||||
const TelepathyCcipReadIsmAbi = [ |
||||
'function verify(bytes, bytes) public view returns (bool)', |
||||
'function step(uint256) external', |
||||
'function syncCommitteePoseidons(uint256) external view returns (bytes32)', |
||||
]; |
||||
|
||||
export { TelepathyCcipReadIsmAbi }; |
@ -1,28 +1,25 @@ |
||||
import { Server } from '@chainlink/ccip-read-server'; |
||||
import { log } from 'console'; |
||||
|
||||
import { ProofsServiceAbi } from './abis/ProofsServiceAbi'; |
||||
import * as config from './config'; |
||||
import { ProofsService } from './services/ProofsService'; |
||||
|
||||
// Initialize Services
|
||||
const proofsService = new ProofsService( |
||||
config.LIGHT_CLIENT_ADDR, |
||||
config.RPC_ADDRESS, |
||||
config.STEP_FN_ID, |
||||
config.CHAIN_ID, |
||||
config.SUCCINCT_PLATFORM_URL, |
||||
config.SUCCINCT_API_KEY, |
||||
); |
||||
|
||||
// Initialize Server and add Service handlers
|
||||
const server = new Server(); |
||||
|
||||
server.add(ProofsServiceAbi, [ |
||||
{ type: 'getProofs', func: proofsService.getProofs.bind(this) }, |
||||
{ |
||||
type: 'getProofs', |
||||
func: async ([target, storageKey, slot]) => { |
||||
const proofsService = new ProofsService( |
||||
config.RPC_ADDRESS, |
||||
config.CONSENSUS_API_URL, |
||||
); |
||||
return proofsService.getProofs(target, storageKey, slot); |
||||
}, |
||||
}, |
||||
]); |
||||
|
||||
// Start Server
|
||||
const app = server.makeApp(config.SERVER_URL_PREFIX); |
||||
app.listen(config.SERVER_PORT, () => |
||||
console.log(`Listening on port ${config.SERVER_PORT}`), |
||||
log(`Listening on port ${config.SERVER_PORT}`), |
||||
); |
||||
|
@ -0,0 +1,9 @@ |
||||
export class ConsensusService { |
||||
constructor(private readonly consensusApiUrl: string) {} |
||||
async getOriginBlockNumberBySlot(slot: string): Promise<number> { |
||||
const response = await fetch(`${this.consensusApiUrl}/${slot}`); |
||||
const responseAsJson = await response.json(); |
||||
|
||||
return responseAsJson.data.message.body.execution_payload.block_number; |
||||
} |
||||
} |
@ -1,37 +0,0 @@ |
||||
import { info } from 'console'; |
||||
|
||||
import { Message, MessageTx } from './explorerTypes'; |
||||
|
||||
// These types are copied from hyperlane-explorer. TODO: export them so this file can use them directly.
|
||||
interface ApiResult<R> { |
||||
status: '0' | '1'; |
||||
message: string; |
||||
result: R; |
||||
} |
||||
|
||||
enum API_ACTION { |
||||
GetMessages = 'get-messages', |
||||
} |
||||
|
||||
class HyperlaneService { |
||||
constructor(readonly baseUrl: string) {} |
||||
|
||||
/** |
||||
* Makes a request to the Explorer API to get the block info by message Id. Throws if request fails, or no results |
||||
* @param id: Message id to look up |
||||
*/ |
||||
async getOriginBlockByMessageId(id: string): Promise<MessageTx> { |
||||
info(`Fetching block for id: ${id}`); |
||||
const response = await fetch( |
||||
`${this.baseUrl}?module=message&action=${API_ACTION.GetMessages}&id=${id}`, |
||||
); |
||||
const responseAsJson: ApiResult<Message[]> = await response.json(); |
||||
if (responseAsJson.status === '1') { |
||||
return responseAsJson.result[0]?.origin; |
||||
} else { |
||||
throw new Error(responseAsJson.message); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export { HyperlaneService }; |
@ -1,99 +0,0 @@ |
||||
import { ethers, utils } from 'ethers'; |
||||
|
||||
import { TelepathyCcipReadIsmAbi } from '../abis/TelepathyCcipReadIsmAbi'; |
||||
|
||||
import { ProofStatus } from './common/ProofStatusEnum'; |
||||
|
||||
export type SuccinctConfig = { |
||||
readonly lightClientAddress: string; |
||||
readonly stepFunctionId: string; |
||||
readonly platformUrl: string; |
||||
readonly apiKey: string; |
||||
}; |
||||
|
||||
// Service that interacts with the LightClient/ISM
|
||||
class LightClientService { |
||||
constructor( |
||||
private readonly lightClientContract: ethers.Contract, // TODO USE TYPECHAIN
|
||||
private succinctConfig: SuccinctConfig, |
||||
) {} |
||||
|
||||
private getSyncCommitteePeriod(slot: bigint): bigint { |
||||
return slot / 8192n; // Slots Per Period
|
||||
} |
||||
|
||||
/** |
||||
* Gets syncCommitteePoseidons from ISM/LightClient |
||||
* @param slot |
||||
* @returns |
||||
*/ |
||||
async getSyncCommitteePoseidons(slot: bigint): Promise<string> { |
||||
return await this.lightClientContract.syncCommitteePoseidons( |
||||
this.getSyncCommitteePeriod(slot), |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Calculates the slot given a timestamp, and the LightClient's configured Genesis Time and Seconds Per Slot |
||||
* @param timestamp timestamp to calculate slot with |
||||
*/ |
||||
async calculateSlot(timestamp: bigint): Promise<bigint> { |
||||
return ( |
||||
(timestamp - (await this.lightClientContract.GENESIS_TIME())) / |
||||
(await this.lightClientContract.SECONDS_PER_SLOT()) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Request the proof from Succinct. |
||||
* @param slot |
||||
* @param syncCommitteePoseidon |
||||
*/ |
||||
async requestProof( |
||||
syncCommitteePoseidon: string, |
||||
slot: bigint, |
||||
): Promise<string> { |
||||
console.log(`Requesting proof for${slot}`); |
||||
|
||||
// Note that Succinct will asynchronously call step() on the ISM/LightClient
|
||||
const telepathyIface = new utils.Interface(TelepathyCcipReadIsmAbi); |
||||
|
||||
const body = { |
||||
chainId: this.lightClientContract.chainId, |
||||
to: this.lightClientContract.address, |
||||
data: telepathyIface.encodeFunctionData('step', [slot]), |
||||
functionId: this.lightClientContract.stepFunctionId, |
||||
input: utils.defaultAbiCoder.encode( |
||||
['bytes32', 'uint64'], |
||||
[syncCommitteePoseidon, slot], |
||||
), |
||||
retry: true, |
||||
}; |
||||
|
||||
const response = await fetch( |
||||
`${this.lightClientContract.platformUrl}/new`, |
||||
{ |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
Authorization: `Bearer ${this.succinctConfig.apiKey}`, |
||||
}, |
||||
body: JSON.stringify(body), |
||||
}, |
||||
); |
||||
const responseAsJson = await response.json(); |
||||
|
||||
return responseAsJson.proof_id; |
||||
} |
||||
|
||||
// @dev in the case of when a proof doesn't exist, the request returns an object of { error: 'failed to get proof' }.
|
||||
// Example: GET https://alpha.succinct.xyz/api/proof/4dfd2802-4edf-4c4f-91db-b2d05eb69791
|
||||
async getProofStatus(proofId: string): Promise<ProofStatus> { |
||||
const response = await fetch( |
||||
`${this.lightClientContract.platformUrl}/${proofId}`, |
||||
); |
||||
const responseAsJson = await response.json(); |
||||
return responseAsJson.status ?? ProofStatus.error; |
||||
} |
||||
} |
||||
|
||||
export { LightClientService, ProofStatus }; |
@ -1,60 +0,0 @@ |
||||
// TODO de-dupe this types with the Explorer by moving them to a shared lib
|
||||
// These were originally imported from the explorer package but there were two issues
|
||||
// 1. The explorer is not structured to be a lib (it's an app)
|
||||
// 2. The explorer's deps on monorepo packages created circular deps leading to transitive deps conflicts
|
||||
|
||||
type Address = string; |
||||
|
||||
export enum MessageStatus { |
||||
Unknown = 'unknown', |
||||
Pending = 'pending', |
||||
Delivered = 'delivered', |
||||
Failing = 'failing', |
||||
} |
||||
|
||||
export interface MessageTxStub { |
||||
timestamp: number; |
||||
hash: string; |
||||
from: Address; |
||||
} |
||||
|
||||
export interface MessageTx extends MessageTxStub { |
||||
to: Address; |
||||
blockHash: string; |
||||
blockNumber: number; |
||||
mailbox: Address; |
||||
nonce: number; |
||||
gasLimit: number; |
||||
gasPrice: number; |
||||
effectiveGasPrice: number; |
||||
gasUsed: number; |
||||
cumulativeGasUsed: number; |
||||
maxFeePerGas: number; |
||||
maxPriorityPerGas: number; |
||||
} |
||||
|
||||
export interface MessageStub { |
||||
status: MessageStatus; |
||||
id: string; // Database id
|
||||
msgId: string; // Message hash
|
||||
nonce: number; // formerly leafIndex
|
||||
sender: Address; |
||||
recipient: Address; |
||||
originChainId: number; |
||||
originDomainId: number; |
||||
destinationChainId: number; |
||||
destinationDomainId: number; |
||||
origin: MessageTxStub; |
||||
destination?: MessageTxStub; |
||||
isPiMsg?: boolean; |
||||
} |
||||
|
||||
export interface Message extends MessageStub { |
||||
body: string; |
||||
decodedBody?: string; |
||||
origin: MessageTx; |
||||
destination?: MessageTx; |
||||
totalGasAmount?: string; |
||||
totalPayment?: string; |
||||
numPayments?: number; |
||||
} |
@ -1,18 +0,0 @@ |
||||
import { describe, expect, test } from '@jest/globals'; |
||||
|
||||
import { HyperlaneService } from '../../src/services/HyperlaneService'; |
||||
|
||||
describe('HyperlaneServiceTest', () => { |
||||
let hyperlaneService: HyperlaneService; |
||||
beforeEach(() => { |
||||
hyperlaneService = new HyperlaneService( |
||||
'https://explorer.hyperlane.xyz/api', |
||||
); |
||||
}); |
||||
test('should get the block by messageId', async () => { |
||||
await hyperlaneService.getOriginBlockByMessageId( |
||||
'0xb0430e396f4014883c01bb3ee43df17ce93d8257a0a0b5778d9d3229a1bf02bb', |
||||
); |
||||
expect(true).toBe(true); |
||||
}); |
||||
}); |
@ -1,29 +0,0 @@ |
||||
import { describe, expect, jest, test } from '@jest/globals'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { TelepathyCcipReadIsmAbi } from '../../src/abis/TelepathyCcipReadIsmAbi'; |
||||
import { LightClientService } from '../../src/services/LightClientService'; |
||||
import { RPCService } from '../../src/services/RPCService'; |
||||
|
||||
describe('LightClientService', () => { |
||||
let lightClientService: LightClientService; |
||||
beforeEach(() => { |
||||
const rpcService = new RPCService('http://localhost:8545'); |
||||
const lightClientContract = new ethers.Contract( |
||||
'lightClientAddress', |
||||
TelepathyCcipReadIsmAbi, |
||||
rpcService.provider, |
||||
); |
||||
lightClientService = new LightClientService(lightClientContract, { |
||||
lightClientAddress: ethers.constants.AddressZero, |
||||
stepFunctionId: ethers.constants.HashZero, |
||||
platformUrl: 'http://localhost:8080', |
||||
apiKey: 'apiKey', |
||||
}); |
||||
|
||||
jest.resetModules(); |
||||
}); |
||||
test('should return the correct proof status', () => { |
||||
expect(lightClientService.calculateSlot(1n)).toBeGreaterThan(0); |
||||
}); |
||||
}); |
Loading…
Reference in new issue