Relayer and UI Cleanup (#3)

* contracts: fix ICircleIntegration interface

ui: fix addresses and number of confirms

offchain-relayer: refactor

* offchain-relayer: fix package.json

Co-authored-by: A5 Pickle <a5-pickle@users.noreply.github.com>
This commit is contained in:
A5 Pickle 2022-12-02 14:15:23 -06:00 committed by GitHub
parent b987f739b6
commit 34cd2c5887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 209 additions and 102 deletions

View File

@ -1,6 +1,6 @@
## NativeSwap
https://certusone.github.io/nativeswap-usdc-example/
https://wormhole-foundation.github.io/example-nativeswap-usdc/
This is a non-production example program.

View File

@ -29,11 +29,10 @@ interface ICircleIntegration {
bytes payload;
}
function transferTokensWithPayload(
TransferParameters memory transferParams,
uint32 batchId,
bytes memory payload
) external payable returns (uint64 messageSequence);
function transferTokensWithPayload(TransferParameters memory transferParams, uint32 batchId, bytes memory payload)
external
payable
returns (uint64 messageSequence);
function redeemTokensWithPayload(RedeemParameters memory params)
external
@ -44,4 +43,6 @@ interface ICircleIntegration {
function getDomainFromChainId(uint16 chainId_) external view returns (uint32);
function getChainIdFromDomain(uint32 domain) external view returns (uint16);
function isMessageConsumed(bytes32 hash) external view returns (bool);
}

View File

@ -14,7 +14,8 @@
"typescript": "^4.8.4"
},
"scripts": {
"build": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts ../contracts/build/contracts/*.json",
"build-types": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts ../contracts/build/contracts/*.json",
"build": "npm run build-types",
"clean": "rm -rf node_modules src/ethers-contracts",
"start": "npm run build && ts-node src/main.ts"
}

View File

@ -11,8 +11,9 @@ import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"
import {
CrossChainSwapV2__factory,
CrossChainSwapV3__factory,
IUSDCIntegration__factory,
ICircleIntegration__factory,
} from "./ethers-contracts";
import { WebSocketProvider } from "./websocket";
const WORMHOLE_RPC_HOSTS = ["https://wormhole-v2-testnet-api.certus.one"];
@ -22,12 +23,22 @@ const WORMHOLE_RPC_HOSTS = ["https://wormhole-v2-testnet-api.certus.one"];
const receiptMaxAttempts = Number(process.env.RECEIPT_MAX_ATTEMPTS!);
const receiptTimeout = Number(process.env.RECEIPT_TIMEOUT!);
const attestationTimeout = Number(process.env.ATTESTATION_TIMEOUT!);
console.log(`attestation timeout: ${attestationTimeout}`);
const attestationMaxAttempts = Number(process.env.ATTESTATION_MAX_ATTEMPTS!);
console.log(`attestation max attempts: ${attestationMaxAttempts}`);
const vaaTimeout = Number(process.env.ATTESTATION_TIMEOUT!);
console.log(`vaa timeout: ${vaaTimeout}`);
const vaaMaxAttempts = Number(process.env.ATTESTATION_MAX_ATTEMPTS!);
console.log(`vaa max attempts: ${vaaMaxAttempts}`);
const srcContractType = process.env.SRC_CONTRACT_TYPE!;
const circleEmitter = process.env.CIRCLE_EMITTER!;
const srcProvider = new ethers.providers.WebSocketProvider(
process.env.SRC_RPC!
);
const srcProvider = new WebSocketProvider(process.env.SRC_RPC!);
const dstWallet = new ethers.Wallet(
process.env.PRIVATE_KEY!,
new ethers.providers.StaticJsonRpcProvider(process.env.DST_RPC!)
@ -56,18 +67,14 @@ const WORMHOLE_RPC_HOSTS = ["https://wormhole-v2-testnet-api.certus.one"];
const srcChainId = await wormhole.chainId().then((id) => id as ChainId);
const wormCircle = await srcContract
.USDC_INTEGRATION()
.then((address) => IUSDCIntegration__factory.connect(address, srcProvider));
.CIRCLE_INTEGRATION()
.then((address) =>
ICircleIntegration__factory.connect(address, srcProvider)
);
// let's go
console.log(
"starting relayer",
srcProvider.network.chainId,
"to",
await dstWallet.getChainId(),
"chainId",
srcChainId
);
console.log("starting relayer");
console.log(`src: wormhole chain id: ${srcChainId}`);
// collect pending transactions
const pendingTxHashes: string[] = [];
@ -81,7 +88,7 @@ const WORMHOLE_RPC_HOSTS = ["https://wormhole-v2-testnet-api.certus.one"];
// process transactions here
while (true) {
while (pendingTxHashes.length > 0) {
if (pendingTxHashes.length > 0) {
// get first transaction hash
const txHash = pendingTxHashes[0];
@ -99,70 +106,90 @@ const WORMHOLE_RPC_HOSTS = ["https://wormhole-v2-testnet-api.certus.one"];
}
// if we really have a receipt, process the logs
if (receipt !== null && receipt.to == srcContract.address) {
console.log("found transaction", receipt.transactionHash);
if (receipt === null || receipt.to != srcContract.address) {
pendingTxHashes.shift();
continue;
}
// find circle message
const [circleBridgeMessage, circleAttestation] =
await handleCircleMessageInLogs(receipt.logs, circleEmitter);
if (circleBridgeMessage === null || circleAttestation === null) {
console.log("we have a problem... ignore?", receipt);
continue;
}
console.log(`found transaction ${txHash}`);
// fetch wormhole message sequence
const sequences = parseSequencesFromLogEth(receipt, wormhole.address);
if (sequences.length == 0) {
console.log("Cannot find any wormhole sequences?", receipt);
continue;
}
const sequence = sequences[0];
// now fetched the signed message
const { vaaBytes: encodedWormholeMessage } =
await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
srcChainId,
getEmitterAddressEth(wormCircle.address),
sequence,
{
transport: NodeHttpTransport(),
}
);
// fetch wormhole message sequence
const sequences = parseSequencesFromLogEth(receipt, wormhole.address);
if (sequences.length == 0) {
console.log(`probably just a redeem. moving on`);
pendingTxHashes.shift();
continue;
}
const sequence = sequences[0];
// now fetched the signed message
const result = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
srcChainId,
getEmitterAddressEth(wormCircle.address),
sequence,
{
const receipt = await dstContract
.recvAndSwapExactNativeIn({
encodedWormholeMessage,
circleBridgeMessage,
circleAttestation,
})
.catch((reason) => {
console.log(reason);
return null;
})
.then((tx) => {
if (tx === null) {
return null;
}
transport: NodeHttpTransport(),
},
vaaTimeout,
vaaMaxAttempts
).catch((reason) => null);
return tx.wait() as Promise<ethers.ContractReceipt>;
});
if (receipt !== null) {
console.log("relayed", receipt.transactionHash);
if (result === null) {
console.log(`cannot find signed vaa for ${txHash}`);
pendingTxHashes.shift();
continue;
}
const { vaaBytes: encodedWormholeMessage } = result;
// find circle message
const [circleBridgeMessage, circleAttestation] =
await handleCircleMessageInLogs(
receipt.logs,
circleEmitter,
attestationTimeout,
attestationMaxAttempts
);
if (circleBridgeMessage === null || circleAttestation === null) {
console.log(`cannot attest Circle message for ${txHash}`);
pendingTxHashes.shift();
continue;
}
const redeemReceipt = await dstContract
.recvAndSwapExactNativeIn({
encodedWormholeMessage,
circleBridgeMessage,
circleAttestation,
})
.catch((reason) => {
console.log(reason);
return null;
})
.then((tx) => {
if (tx === null) {
return null;
}
}
return tx.wait() as Promise<ethers.ContractReceipt>;
});
if (redeemReceipt !== null) {
console.log(`relayed ${redeemReceipt.transactionHash}`);
}
pendingTxHashes.shift();
}
await sleep(relayerTimeout);
}
})();
async function handleCircleMessageInLogs(
logs: ethers.providers.Log[],
circleEmitterAddress: string
circleEmitterAddress: string,
attestationTimeout: number,
attestationMaxAttempts: number
): Promise<[string | null, string | null]> {
const circleMessage = await findCircleMessageInLogs(
logs,
@ -173,7 +200,14 @@ async function handleCircleMessageInLogs(
}
const circleMessageHash = ethers.utils.keccak256(circleMessage);
const signature = await getCircleAttestation(circleMessageHash);
const signature = await getCircleAttestation(
circleMessageHash,
attestationTimeout,
attestationMaxAttempts
);
if (signature === null) {
return [null, null];
}
return [circleMessage, signature];
}
@ -200,9 +234,11 @@ async function sleep(timeout: number) {
async function getCircleAttestation(
messageHash: ethers.BytesLike,
timeout: number = 2000
timeout: number,
maxAttempts: number
) {
while (true) {
//while (true) {
for (let i = 0; i < maxAttempts; ++i) {
// get the post
const response = await axios
.get(`https://iris-api-sandbox.circle.com/attestations/${messageHash}`)
@ -227,4 +263,6 @@ async function getCircleAttestation(
await sleep(timeout);
}
return null;
}

View File

@ -0,0 +1,83 @@
import { ethers } from "ethers";
const WEBSOCKET_PING_INTERVAL = 10000;
const WEBSOCKET_PONG_TIMEOUT = 5000;
const WEBSOCKET_RECONNECT_DELAY = 100;
const WebSocketProviderClass =
(): new () => ethers.providers.WebSocketProvider => class {} as never;
export class WebSocketProvider extends WebSocketProviderClass() {
private provider?: ethers.providers.WebSocketProvider;
private events: ethers.providers.WebSocketProvider["_events"] = [];
private requests: ethers.providers.WebSocketProvider["_requests"] = {};
private handler = {
get(target: WebSocketProvider, prop: string, receiver: unknown) {
const value =
target.provider && Reflect.get(target.provider, prop, receiver);
return value instanceof Function ? value.bind(target.provider) : value;
},
};
constructor(private providerUrl: any) {
super();
this.create();
return new Proxy(this, this.handler);
}
private create() {
if (this.provider) {
this.events = [...this.events, ...this.provider._events];
this.requests = { ...this.requests, ...this.provider._requests };
}
const provider = new ethers.providers.WebSocketProvider(
this.providerUrl,
this.provider?.network?.chainId
);
let pingInterval: NodeJS.Timer | undefined;
let pongTimeout: NodeJS.Timeout | undefined;
provider._websocket.on("open", () => {
pingInterval = setInterval(() => {
provider._websocket.ping();
pongTimeout = setTimeout(() => {
provider._websocket.terminate();
}, WEBSOCKET_PONG_TIMEOUT);
}, WEBSOCKET_PING_INTERVAL);
let event;
while ((event = this.events.pop())) {
provider._events.push(event);
provider._startEvent(event);
}
for (const key in this.requests) {
provider._requests[key] = this.requests[key];
provider._websocket.send(this.requests[key].payload);
delete this.requests[key];
}
});
provider._websocket.on("pong", () => {
if (pongTimeout) clearTimeout(pongTimeout);
});
provider._websocket.on("close", (code: number) => {
provider._wsReady = false;
if (pingInterval) clearInterval(pingInterval);
if (pongTimeout) clearTimeout(pongTimeout);
if (code !== 1000) {
setTimeout(() => this.create(), WEBSOCKET_RECONNECT_DELAY);
}
});
this.provider = provider;
}
}

View File

@ -60,7 +60,7 @@ export default function TransactionProgress({
? currentBlock - txBlockNumber
: 0;
const expectedBlocks =
chainId === CHAIN_ID_POLYGON ? 512 : CHAIN_ID_AVAX ? 1 : 15;
chainId === CHAIN_ID_POLYGON ? 512 : 1; //CHAIN_ID_AVAX ? 1 : 15;
blockDiff = Math.min(Math.max(blockDiff, 0), expectedBlocks);
let value;
let valueBuffer;

View File

@ -20,8 +20,8 @@ import {
UniswapToUniswapQuoter,
} from "../route/cross-quote";
import {
CIRCLE_EMITTER_ADDRESS_ETHEREUM,
CIRCLE_EMITTER_ADDRESS_AVALANCHE,
CIRCLE_INTEGRATION_ADDRESS_ETHEREUM,
CIRCLE_INTEGRATION_ADDRESS_AVALANCHE,
CORE_BRIDGE_ADDRESS_ETHEREUM,
CORE_BRIDGE_ADDRESS_AVALANCHE,
WORMHOLE_RPC_HOSTS,
@ -74,7 +74,7 @@ const EXECUTION_PARAMETERS_ETHEREUM: ExecutionParameters = {
wormhole: {
chainId: CHAIN_ID_ETH,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_ETHEREUM,
circleEmitterAddress: CIRCLE_EMITTER_ADDRESS_ETHEREUM,
circleEmitterAddress: CIRCLE_INTEGRATION_ADDRESS_ETHEREUM,
},
};
@ -85,7 +85,7 @@ const EXECUTION_PARAMETERS_AVALANCHE: ExecutionParameters = {
wormhole: {
chainId: CHAIN_ID_AVAX,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_AVALANCHE,
circleEmitterAddress: CIRCLE_EMITTER_ADDRESS_AVALANCHE,
circleEmitterAddress: CIRCLE_INTEGRATION_ADDRESS_AVALANCHE,
},
};

View File

@ -86,11 +86,11 @@ export const CORE_BRIDGE_ADDRESS_AVALANCHE =
"0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C";
// circle integration
export const CIRCLE_EMITTER_ADDRESS_ETHEREUM =
"0xdbedb4ebd098e9f1777af9f8088e794d381309d1";
export const CIRCLE_INTEGRATION_ADDRESS_ETHEREUM =
"0xbdCc4eBE3157df347671e078a41eE5Ce137Cd306";
export const CIRCLE_EMITTER_ADDRESS_AVALANCHE =
"0x3e6a4543165aaecbf7ffc81e54a1c7939cb12cb8";
export const CIRCLE_INTEGRATION_ADDRESS_AVALANCHE =
"0xb200977d46aea35ce6368d181534f413570a0f54";
// gas
export const APPROVAL_GAS_LIMIT = "100000";

View File

@ -43,7 +43,7 @@ import SwapProgress from "../components/SwapProgress";
import Footer from "../components/Footer";
import TerraWalletKey from "../components/TerraWalletKey";
import useIsWalletReady from "../hooks/useIsWalletReady";
import { IWormhole__factory, IUSDCIntegration__factory } from "../ethers-contracts";
import { IWormhole__factory, ICircleIntegration__factory } from "../ethers-contracts";
const useStyles = makeStyles((theme) => ({
bg: {
@ -408,7 +408,7 @@ export default function Home() {
provider
).parseVM(vaaBytes);
const circleEmitter = IUSDCIntegration__factory.connect(
const circleEmitter = ICircleIntegration__factory.connect(
executor.dstExecutionParams.wormhole.circleEmitterAddress,
executor.quoter.getDstEvmProvider()!,
)
@ -605,15 +605,7 @@ export default function Home() {
Goerli Faucet
</Link>
<Link
href="https://faucet.polygon.technology/"
target="_blank"
rel="noopener noreferrer"
style={{ margin: "5px" }}
>
Mumbai Faucet
</Link>
<Link
href="https://faucet.avax-test.network/"
href="https://faucet.avax.network/"
target="_blank"
rel="noopener noreferrer"
style={{ margin: "5px" }}
@ -621,15 +613,7 @@ export default function Home() {
Fuji Faucet
</Link>
<Link
href="https://testnet.binance.org/faucet-smart/"
target="_blank"
rel="noopener noreferrer"
style={{ margin: "5px" }}
>
BSC Faucet
</Link>
<Link
href="https://github.com/certusone/nativeswap-usdc-example/"
href="https://github.com/wormhole-foundation/example-nativeswap-usdc/"
target="_blank"
rel="noopener noreferrer"
style={{ margin: "5px" }}