Skip to content

Commit

Permalink
ft_watcher: worker to invoke ft
Browse files Browse the repository at this point in the history
test: initial swap monitor testing

ft_swap: parse redeem params to get fill vaa

add id for redeem event

ft_watcher: parse swap event and input

ft_watcher: plug swap layer into ft watcher

Signed-off-by: bingyuyap <[email protected]>
  • Loading branch information
bingyuyap committed Aug 7, 2024
1 parent de8f1f8 commit 5a9ad81
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 10 deletions.
3 changes: 2 additions & 1 deletion watcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"read-firestore": "ts-node scripts/readFirestore.ts",
"reconstruct-vaa": "ts-node scripts/reconstructVAA.ts",
"update-found-vaas": "ts-node scripts/updateFoundVAAs.ts",
"update-rows": "ts-node scripts/updateRows.ts"
"update-rows": "ts-node scripts/updateRows.ts",
"swap": "ts-node scripts/swapLayer.ts"
},
"dependencies": {
"@celo-tools/celo-ethers-wrapper": "^0.3.0",
Expand Down
49 changes: 49 additions & 0 deletions watcher/scripts/decodeLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ethers } from "ethers";

function decodeLog(log: any) {
console.log("Decoding log:");
console.log("Contract address:", log.address);

console.log("\nTopics:");
console.log("Event signature:", log.topics[0]);
console.log("Indexed parameter:", log.topics[1]);

console.log("\nDecoding indexed parameter:");
const indexedAddress = ethers.utils.getAddress('0x' + log.topics[1].slice(-40));
console.log("Address:", indexedAddress);

console.log("\nDecoding data:");
const decodedData = ethers.utils.defaultAbiCoder.decode(
['address', 'uint256', 'uint256'],
log.data
);

console.log("Token address:", decodedData[0]);
console.log("Amount:", decodedData[1].toString());
console.log("Fee:", decodedData[2].toString());

// Try to identify the event
const eventSignature = "Redeemed(address,address,uint256,uint256)";
const calculatedEventHash = ethers.utils.id(eventSignature);

console.log("\nEvent identification:");
console.log("Calculated hash for 'Redeemed(address,address,uint256,uint256)':", calculatedEventHash);
console.log("Matches log topic 0:", calculatedEventHash === log.topics[0]);
}

// Log data
const logData = {
transactionIndex: 0,
blockNumber: 20034945,
transactionHash: '0xcbb19feeadaa8949d5d2a3f253ecfd94d329a5fc6165c87acb297c6f44e19755',
address: '0xdA11B3bc8705D84BEae4a796035bDcCc9b59d1ee',
topics: [
'0x5cdf07ad0fc222442720b108e3ed4c4640f0fadc2ab2253e66f259a0fea83480',
'0x00000000000000000000000095ced938f7991cd0dfcb48f0a06a40fa1af46ebc'
],
data: '0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000004a817c8000000000000000000000000000000000000000000000000000000000000000000',
logIndex: 6,
blockHash: '0x8e9a4f48e546768f8d88a148702d83ba52b15da1c7377ef1d0b1514989b62cbd'
};

decodeLog(logData);
70 changes: 70 additions & 0 deletions watcher/scripts/swapLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { parseVaa } from '@wormhole-foundation/wormhole-monitor-common';
import { ethers } from 'ethers';
const provider = new ethers.providers.JsonRpcProvider('http://localhost:8548');

const redeemEvent = "Redeemed(address,address,uint256,uint256)";
const redeemEventHash = ethers.utils.id(redeemEvent);
const redeemInterface = new ethers.utils.Interface([
"event Redeemed(address indexed recipient, address outputToken, uint256 outputAmount, uint256 relayingFee)"
]);

async function logBlkTxs(blockNumber: number) {
// Get a block by number
const block = await provider.getBlock(blockNumber);

// Get all transactions in a block
await Promise.all(
block.transactions.map((txHash) => logTx(txHash)));
}

async function logTx(txHash: string) {
const tx = await provider.getTransaction(txHash);
console.log('tx',tx)
const receipt = await provider.getTransactionReceipt(txHash);
const REDEEM_SELECTOR = '0x604009a9';
if (tx.data.startsWith(REDEEM_SELECTOR)) {
console.log('This transaction calls the redeem function');
console.log(receipt);
// Remove the function selector (first 4 bytes)
const inputData = '0x' + tx.data.slice(10);


// Use AbiCoder to decode the raw input data
const abiCoder = new ethers.utils.AbiCoder();

try {
const decodedInput = abiCoder.decode(['bytes', 'tuple(bytes, bytes, bytes)'], inputData);

// If you want to further parse the encodedWormholeMessage
const encodedWormholeMessage = decodedInput[1][0];
if (encodedWormholeMessage && encodedWormholeMessage.length >= 8) {
const vaaBytes = Buffer.from(encodedWormholeMessage.slice(2), 'hex'); // Remove '0x' if present
const parsedVaa = parseVaa(vaaBytes);

console.log(parsedVaa);
}
} catch (error) {
console.error('Error decoding input data:', error);
}

const redeemLogs = receipt.logs.filter((log) => log.topics[0] === redeemEventHash);

console.log(redeemLogs[0])
// There should only be one redeem invocation
if (redeemLogs.length != 1) throw new Error(`redeem has ${redeemLogs.length} logs`)
const decodedLog = redeemInterface.parseLog(redeemLogs[0]);
} else {
console.log('This transaction does not call the redeem function');
}
}
async function logTxs() {
// Get the latest block number
const blockNumber = await provider.getBlockNumber();

for (let i = 20034947; i <= blockNumber; ++i) {
await logBlkTxs(i);
console.log('checked block', i);
}
}

logTxs();
1 change: 1 addition & 0 deletions watcher/src/databases/BigtableDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class BigtableDatabase extends Database {
this.signedVAAsTableId = assertEnvironmentVariable('BIGTABLE_SIGNED_VAAS_TABLE_ID');
this.vaasByTxHashTableId = assertEnvironmentVariable('BIGTABLE_VAAS_BY_TX_HASH_TABLE_ID');
this.instanceId = assertEnvironmentVariable('BIGTABLE_INSTANCE_ID');
// TODO: make these const?
this.latestCollectionName = assertEnvironmentVariable('FIRESTORE_LATEST_COLLECTION');
this.latestNTTCollectionName = assertEnvironmentVariable('FIRESTORE_LATEST_NTT_COLLECTION');
this.latestFTCollectionName = assertEnvironmentVariable('FIRESTORE_LATEST_FT_COLLECTION');
Expand Down
13 changes: 12 additions & 1 deletion watcher/src/fastTransfer/consts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TokenRouter } from '@wormhole-foundation/example-liquidity-layer-evm/dist/cjs/src';
import { Chain, Network } from '@wormhole-foundation/sdk-base';

export type FastTransferContracts = 'MatchingEngine' | 'TokenRouter' | 'USDCMint';
Expand All @@ -16,14 +17,19 @@ export interface SolanaContractAddresses {
export interface EthereumContractAddresses {
TokenRouter: string;
CircleBridge?: string;
// Devnet has no swap layer as they need the mainnet qutoes from Uniswap
SwapLayer?: string;
}

type NetworkWithLocalnet = Network | 'Localnet';

export type ContractAddresses = SolanaContractAddresses | EthereumContractAddresses;

export type FastTransferContractAddresses = {
[key in Network]?: {
[key in NetworkWithLocalnet]?: {
Solana?: SolanaContractAddresses;
ArbitrumSepolia?: EthereumContractAddresses;
Ethereum?: EthereumContractAddresses;
};
};

Expand All @@ -41,6 +47,11 @@ export const FAST_TRANSFER_CONTRACTS: FastTransferContractAddresses = {
CircleBridge: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5',
},
},
Localnet: {
Ethereum: {
TokenRouter: '0x92E813b6BAf1d17618586118C1a3CffFE2D283Dc'
}
}
};

// Will add more chains as needed
Expand Down
116 changes: 116 additions & 0 deletions watcher/src/fastTransfer/swapLayer/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { ethers } from 'ethers';
import { RedeemSwap } from '../types';
import { parseVaa } from '@wormhole-foundation/wormhole-monitor-common';

class SwapLayerParser {
private provider: ethers.providers.JsonRpcProvider;
private swapLayerAddress: string;
private swapLayerInterface: ethers.utils.Interface;

constructor(provider: ethers.providers.JsonRpcProvider, swapLayerAddress: string) {
this.provider = provider;
this.swapLayerAddress = swapLayerAddress;
this.swapLayerInterface = new ethers.utils.Interface([
"event Redeemed(address indexed recipient, address outputToken, uint256 outputAmount, uint256 relayingFee)"
]);
}

async parseSwapLayerTransaction(txHash: string, blockTime: number): Promise<RedeemSwap | null> {
const receipt = await this.provider.getTransactionReceipt(txHash);

const tx = await this.provider.getTransaction(txHash);
if (!receipt || !tx) return null;

// Remove the function selector (first 4 bytes)
const inputData = '0x' + tx.data.slice(10);


// Use AbiCoder to decode the raw input data
let fillVaaId: string = ''
const abiCoder = new ethers.utils.AbiCoder();
try {
const decodedInput = abiCoder.decode(['bytes', 'tuple(bytes, bytes, bytes)'], inputData);

// If you want to further parse the encodedWormholeMessage
const encodedWormholeMessage = decodedInput[1][0];
if (encodedWormholeMessage && encodedWormholeMessage.length >= 8) {
const vaaBytes = Buffer.from(encodedWormholeMessage.slice(2), 'hex'); // Remove '0x' if present
const parsedVaa = parseVaa(vaaBytes);

fillVaaId = `${parsedVaa.emitterChain}/${parsedVaa.emitterAddress.toString('hex')}/${parsedVaa.sequence}`;
}
} catch (error) {
console.error('Error decoding input data:', error);
}

const swapEvent = receipt.logs
.filter(log => log.address.toLowerCase() === this.swapLayerAddress.toLowerCase())
.map(log => {
try {
return this.swapLayerInterface.parseLog(log);
} catch (e) {
return null;
}
})
.find(event => event && event.name === 'Redeemed');

if (!swapEvent) return null;

return {
tx_hash: txHash,
recipient: swapEvent.args.recipient,
output_amount: swapEvent.args.outputAmount.toString(),
output_token: swapEvent.args.outputToken,
timestamp: new Date(blockTime * 1000),
relaying_fee: swapEvent.args.relayingFee.toString(),
fill_vaa_id: fillVaaId
};
}

async getFTSwapInRange(fromBlock: number, toBlock: number): Promise<{
results: RedeemSwap[];
lastBlockTime: number;
}> {
const filter = {
address: this.swapLayerAddress,
fromBlock,
toBlock,
topics: [this.swapLayerInterface.getEventTopic('Redeemed')]
};

const logs = await this.provider.getLogs(filter);

const blocks: Map<number, ethers.providers.Block> = new Map();

const results = await Promise.all(
logs.map(async log => {
const blockTime = await this.fetchBlockTime(blocks, log.blockNumber);
const txHash = log.transactionHash;
return this.parseSwapLayerTransaction(txHash, blockTime);
})
);

const lastBlock = await this.provider.getBlock(toBlock);

return {
results: results.filter((result): result is RedeemSwap => result !== null),
lastBlockTime: lastBlock.timestamp
};
}

private async fetchBlockTime(
blocks: Map<number, ethers.providers.Block>,
blockNumber: number
): Promise<number> {
let block = blocks.get(blockNumber);
if (!block) {
block = await this.provider.getBlock(blockNumber);
blocks.set(blockNumber, block);
}
return block.timestamp;
}
}

export default SwapLayerParser;


12 changes: 11 additions & 1 deletion watcher/src/fastTransfer/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js'; // Imported since FT codebase uses BN

import { ChainId } from '@wormhole-foundation/sdk-base';
// Type definitions are snake_case to match the database schema
export enum FastTransferProtocol {
CCTP = 'cctp',
Expand Down Expand Up @@ -142,3 +142,13 @@ export type AuctionUpdatedEvent = {
name: 'AuctionUpdated';
data: AuctionUpdated;
};

export type RedeemSwap = {
tx_hash: string;
recipient: string;
output_token: string;
output_amount: string;
relaying_fee: string;
timestamp: Date;
fill_vaa_id: string;
}
Loading

0 comments on commit 5a9ad81

Please sign in to comment.