Skip to content

Fetch offchain data aptos #22

New issue

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

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

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
.env*
coverage
coverage-summary.txt
.vscode
8 changes: 8 additions & 0 deletions src/lib/attestation/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Attestation = {
attestation: string
messageHash: string
}

export interface AttestationClient {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small abstraction over the atts. clients, in case more come in

getAttestation(hash: string): Promise<Attestation>
}
3 changes: 3 additions & 0 deletions src/lib/attestation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './usdc.ts'
export * from './lbtc.ts'
export { type AttestationClient } from './client.ts'
64 changes: 64 additions & 0 deletions src/lib/attestation/lbtc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Attestation, AttestationClient } from './client.ts'

const LOMBARD_API_URL = {
mainnet: 'https://mainnet.prod.lombard.finance',
testnet: 'https://gastald-testnet.prod.lombard.finance',
}

type LombardAttestation =
| { status: 'NOTARIZATION_STATUS_SESSION_APPROVED'; message_hash: string; attestation: string }
| { status: string; message_hash: string }
type AttestationsResponse = { attestations: Array<LombardAttestation> }

export class LBTCAttestationClient implements AttestationClient {
private readonly url: string

constructor(isTestnet: boolean) {
this.url = isTestnet ? LOMBARD_API_URL.testnet : LOMBARD_API_URL.mainnet
}

async getAttestation(hash: string): Promise<Attestation> {
try {
const res = await fetch(`${this.url}/api/bridge/v1/deposits/getByHash`, {
method: 'POST',
body: JSON.stringify({ messageHash: [hash] }),
})
const json = (await res.json()) as AttestationsResponse

const attestation = this.validateReponse(json, hash)
return { attestation, messageHash: hash }
} catch (e) {
throw new Error(
`Error while fetching for USDC attestation with CIRCLE: ${(e as Error)?.message}`,
)
}
}

private validateReponse(response: AttestationsResponse, hash: string): string {
// Ideally done with zod
if (response == null || !('attestations' in response)) {
throw new Error(
'Error while fetching LBTC attestation. Response: ' + JSON.stringify(response, null, 2),
)
}
const attestation = response.attestations.find((att) => att.message_hash === hash)
if (attestation == null) {
throw new Error(
'Could not find requested LBTC attestation with hash:' +
hash +
' in response: ' +
JSON.stringify(response, null, 2),
)
}
if (
attestation.status === 'NOTARIZATION_STATUS_SESSION_APPROVED' &&
'attestation' in attestation
) {
return attestation.attestation
}
throw new Error(
'LBTC attestation is not approved or invalid. Response: ' +
JSON.stringify(attestation, null, 2),
)
}
}
43 changes: 43 additions & 0 deletions src/lib/attestation/usdc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Attestation, AttestationClient } from './client.ts'

// Docs: https://developers.circle.com/api-reference/stablecoins/common/get-attestation
const CIRCLE_API_URL = {
mainnet: 'https://iris-api.circle.com/v1',
testnet: 'https://iris-api-sandbox.circle.com/v1',
}

type AttestationResponse =
| { error: 'string' }
| { status: 'pending_confirmations' }
| { status: 'complete'; attestation: string }

export class USDCAttestationClient implements AttestationClient {
private readonly url: string

constructor(isTestnet: boolean) {
this.url = isTestnet ? CIRCLE_API_URL.testnet : CIRCLE_API_URL.mainnet
}

async getAttestation(hash: string): Promise<Attestation> {
try {
const res = await fetch(`${this.url}/attestations/${hash}`)
const json = (await res.json()) as AttestationResponse
const attestation = this.validateReponse(json)
return { attestation, messageHash: hash }
} catch (e) {
throw new Error(
`Error while fetching for USDC attestation with CIRCLE: ${(e as Error)?.message}`,
)
}
}

private validateReponse(json: AttestationResponse): string {
// Ideally we do this with zod or similar
if (!('status' in json) || json.status !== 'complete' || !json.attestation) {
throw new Error(
'Could not fetch USDC attestation. Response: ' + JSON.stringify(json, null, 2),
)
}
return json.attestation
}
}
46 changes: 46 additions & 0 deletions src/lib/events/aptos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ChainEvent } from './types.ts'

// Reference https://github.com/aptos-labs/aptos-ts-sdk/blob/main/src/types/generated/operations.ts#L432
// We should get this from the aptos-ts-sdk
export type AptosEvent = {
account_address: string
creation_number: unknown
data: unknown
event_index: unknown
sequence_number: unknown
transaction_block_height: unknown
transaction_version: unknown
type: string // something like "0x1::coin::WithdrawEvent",
indexed_type: string
}

export const toChainEventFromAptos = (event: AptosEvent): ChainEvent => {
return {
id: event.type,
index: event.event_index as number,
address: event.account_address,
data: event.data as string,
indexedArgs: [],
}
}

// TODO: Build specific USDC and LBTC event identification fns
export const isAptosUSDCEvent = (_event: ChainEvent): boolean => {
return false
}

export const isAptosTransferEvent = (_event: ChainEvent): boolean => {
return false
}

export const isAptosBurnedEvent = (_event: ChainEvent): boolean => {
return false
}

export const isAptosLBTCEvent = (_event: ChainEvent): boolean => {
return false
}

export const getEVMLBTCDepositHashes = (_event: ChainEvent): string => {
return ''
}
28 changes: 28 additions & 0 deletions src/lib/events/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
getEVMLBTCDepositHashes,
isEVMBurnedEvent,
isEVMLBTCEvent,
isEVMTransferEvent,
isEVMUSDCEvent,
} from './evm.ts'
import type { ChainEvent } from './types.ts'

export const isUSDCEvent = (event: ChainEvent): boolean => {
return isEVMUSDCEvent(event) // || isAptosUSDCEvent(event) ...
}

export const isTransferEvent = (event: ChainEvent): boolean => {
return isEVMTransferEvent(event)
}

export const isBurnedEvent = (event: ChainEvent): boolean => {
return isEVMBurnedEvent(event)
}

export const isLBTCEvent = (event: ChainEvent): boolean => {
return isEVMLBTCEvent(event)
}

export const getLBTCDepositHashes = (event: ChainEvent): string => {
return getEVMLBTCDepositHashes(event)
}
53 changes: 53 additions & 0 deletions src/lib/events/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type Log, EventFragment, Interface } from 'ethers'
import TokenPoolABI from '../../abi/BurnMintTokenPool_1_5_1.ts'
import { lazyCached } from '../utils.ts'
import type { ChainEvent } from './types.ts'

export const toChainEventFromEVM = (
event: Pick<Log, 'topics' | 'index' | 'address' | 'data'>,
): ChainEvent => {
return {
id: event.topics[0],
index: event.index,
address: event.address,
data: event.data,
indexedArgs: event.topics.slice(1),
}
}

const TokenPoolInterface = lazyCached(
`Interface BurnMintTokenPool 1.5.1`,
() => new Interface(TokenPoolABI),
)
const BURNED_EVENT = TokenPoolInterface.getEvent('Burned')!
const USDC_EVENT = EventFragment.from('MessageSent(bytes message)')
const TRANSFER_EVENT = EventFragment.from('Transfer(address from, address to, uint256 value)')
const LBTC_EVENT = EventFragment.from(
'DepositToBridge(address fromAddress, bytes32 toAddress, bytes32 payloadHash, bytes payload)',
)

export const isEVMUSDCEvent = (event: ChainEvent): boolean => {
return event.id === USDC_EVENT.topicHash
}

export const isEVMTransferEvent = (event: ChainEvent): boolean => {
return event.id === TRANSFER_EVENT.topicHash
}

export const isEVMBurnedEvent = (event: ChainEvent): boolean => {
return event.id === BURNED_EVENT.topicHash
}

export const isEVMLBTCEvent = (event: ChainEvent): boolean => {
return event.id === LBTC_EVENT.topicHash
}

export const getEVMLBTCDepositHashes = (event: ChainEvent): string => {
if (!isEVMLBTCEvent(event)) {
throw new Error('Event is not a LiquidBTC deposit event')
}
if (event.indexedArgs.length < 3) {
throw new Error('Event does not have a deposit hash')
}
return event.indexedArgs[2]
}
4 changes: 4 additions & 0 deletions src/lib/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { type ChainEvent } from './types.ts'
export { toChainEventFromAptos } from './aptos.ts'
export { toChainEventFromEVM } from './evm.ts'
export * from './events.ts'
8 changes: 8 additions & 0 deletions src/lib/events/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Common type able to represent an event from different chains
export type ChainEvent = {
id: string // eventSignature or topics[0] in EVM chains
index: number // position of the event in the transaction
address: string // account emitting the event
data: string // data of the event
indexedArgs: Array<string> // indexed arguments of the event. topics[1:] in EVM chains
}
Loading