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:
parent
b987f739b6
commit
34cd2c5887
|
@ -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.
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" }}
|
||||
|
|
Loading…
Reference in New Issue