wormhole-nativeswap-example/swap_relayer/src/terra.ts

358 lines
9.3 KiB
TypeScript

import {
CHAIN_ID_TERRA,
getIsTransferCompletedTerra,
redeemOnTerra,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import axios from "axios";
import { bech32 } from "bech32";
import { zeroPad } from "ethers/lib/utils";
import { fromUint8Array } from "js-base64";
import * as Terra from "@terra-money/terra.js";
import { logger, OurEnvironment, Type3Payload } from "./index";
export type TerraEnvironment = {
terra_enabled: boolean;
terra_provider_url: string;
terra_chain_id: string;
terra_name: string;
terra_contract_address: string;
terra_token_bridge_address: string;
terra_wallet_private_key: string;
terra_gas_price_url: string;
};
type TerraContractData = {
name: string;
contractAddress: string;
encodedContractAddress: string;
tokenBridgeAddress: string;
lcdConfig: Terra.LCDClientConfig;
lcdClient: Terra.LCDClient;
wallet: Terra.Wallet;
gasPriceUrl: string;
};
let terraContractData: TerraContractData = null;
export function loadTerraConfig(): TerraEnvironment {
let terra_enabled: boolean = false;
if (process.env.TERRA_PROVIDER) {
terra_enabled = true;
if (!process.env.TERRA_CHAIN_ID) {
logger.error("Missing environment variable WALLET_PRIVATE_KEY");
return undefined;
}
if (!process.env.TERRA_NAME) {
logger.error("Missing environment variable WALLET_PRIVATE_KEY");
return undefined;
}
if (!process.env.TERRA_WALLET_PRIVATE_KEY) {
logger.error("Missing environment variable TERRA_WALLET_PRIVATE_KEY");
return undefined;
}
if (!process.env.TERRA_GAS_PRICE_URL) {
logger.error("Missing environment variable TERRA_GAS_PRICE_URL");
return undefined;
}
if (!process.env.TERRA_CONTRACT_ADDRESS) {
logger.error("Missing environment variable TERRA_CONTRACT_ADDRESS");
return undefined;
}
if (!process.env.TERRA_TOKEN_BRIDGE_ADDRESS) {
logger.error("Missing environment variable TERRA_TOKEN_BRIDGE_ADDRESS");
return undefined;
}
}
return {
terra_enabled: terra_enabled,
terra_provider_url: process.env.TERRA_PROVIDER,
terra_chain_id: process.env.TERRA_CHAIN_ID,
terra_name: process.env.TERRA_NAME,
terra_contract_address: process.env.TERRA_CONTRACT_ADDRESS,
terra_token_bridge_address: process.env.TERRA_TOKEN_BRIDGE_ADDRESS,
terra_wallet_private_key: process.env.TERRA_WALLET_PRIVATE_KEY,
terra_gas_price_url: process.env.TERRA_GAS_PRICE_URL,
};
}
export function makeTerraContractData(env: TerraEnvironment) {
if (!env.terra_enabled) return;
let contractAddress: string = env.terra_contract_address.toLowerCase();
if (contractAddress.search("0x") == 0) {
contractAddress = contractAddress.substring(2);
}
let encodedContractAddress: string = Buffer.from(
zeroPad(bech32.fromWords(bech32.decode(contractAddress).words), 32)
).toString("hex");
logger.info(
"Connecting to Terra: contract address: [" +
contractAddress +
"], encoded contract address: [" +
encodedContractAddress +
"], node: [" +
env.terra_provider_url +
"], token bridge address: [" +
env.terra_token_bridge_address +
"], terra chain id: [" +
env.terra_chain_id +
"], terra name: [" +
env.terra_name +
"]"
);
const lcdConfig = {
URL: env.terra_provider_url,
chainID: env.terra_chain_id,
name: env.terra_name,
};
const lcdClient = new Terra.LCDClient(lcdConfig);
const mk = new Terra.MnemonicKey({
mnemonic: env.terra_wallet_private_key,
});
const wallet = lcdClient.wallet(mk);
terraContractData = {
name: "Terra",
contractAddress: contractAddress,
encodedContractAddress: encodedContractAddress,
tokenBridgeAddress: env.terra_token_bridge_address,
lcdConfig: lcdConfig,
lcdClient: lcdClient,
wallet: wallet,
gasPriceUrl: env.terra_gas_price_url,
};
}
export function isTerraContract(
contractAddress: string,
chain_id: number
): boolean {
if (chain_id !== CHAIN_ID_TERRA) return false;
if (!terraContractData) return false;
let retVal: boolean =
terraContractData &&
contractAddress === terraContractData.encodedContractAddress;
logger.debug(
"isTerraContract: comparing [" +
contractAddress +
"] to [" +
terraContractData.encodedContractAddress +
"]: " +
retVal
);
return retVal;
}
export async function relayVaaToTerra(
t3Payload: Type3Payload,
vaaBytes: string
) {
if (!terraContractData) return;
// logger.debug(
// "relayVaaToTerra: checking if already redeemed using tokenBridgeAddress [" +
// terraContractData.tokenBridgeAddress +
// "]"
// );
// if (await isRedeemedOnTerra(terraContractData, vaaBytes)) {
// logger.info(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: completed: already redeemed"
// );
// return;
// }
try {
logger.debug(
"relayVaaToTerra: creating message using contract address [" +
terraContractData.contractAddress +
"]"
);
logger.debug(
"relayVaaToTerra: vaa as hex: [" +
Buffer.from(vaaBytes, "hex").toString("hex") +
"]"
);
logger.debug(
"relayVaaToTerra: vaa as base64: [" +
Buffer.from(vaaBytes, "hex").toString("base64") +
"]"
);
const msg = new Terra.MsgExecuteContract(
terraContractData.wallet.key.accAddress,
terraContractData.contractAddress,
{
submit_vaa: {
data: Buffer.from(vaaBytes, "hex").toString("base64"),
},
}
);
// logger.debug("relayVaaToTerra: getting gas prices");
// const gasPrices = terraContractData.lcdClient.config.gasPrices;
// logger.debug("relayVaaToTerra: estimating fees");
// const feeEstimate = await terraContractData.lcdClient.tx.estimateFee(terraContractData.wallet.key.accAddress, [msg], {
// feeDenoms: ["uluna"],
// gasPrices,
// });
logger.debug("relayVaaToTerra: creating transaction");
const tx = await terraContractData.wallet.createAndSignTx({
msgs: [msg],
memo: "swap relayer",
feeDenoms: ["uluna"],
// gasPrices,
// fee: feeEstimate,
});
logger.info(
"relayVaaToTerra: contract: [" +
t3Payload.contractAddress +
"]: submitting redeem request"
);
const receipt = await terraContractData.lcdClient.tx.broadcast(tx);
logger.info(
"relayVaaToTerra: srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"]: completed: success: %o",
receipt
);
// logger.info(
// "relayVaaToTerra: contract: [" +
// t3Payload.contractAddress +
// "]: success, txHash: " +
// receipt.transactionHash
// );
} catch (e: any) {
// if (await isRedeemedOnTerra(terraContractData, vaaBytes)) {
// logger.info(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: completed: relay failed because the vaa has already been redeemed"
// );
// return;
// }
logger.error(
"relayVaaToTerra: srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"]: completed: transaction failed: %o",
e
);
}
// if (await isRedeemedOnTerra(terraContractData, vaaBytes)) {
// logger.info(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: redeem confirmed"
// );
// } else {
// logger.error(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: completed: failed to confirm redeem!"
// );
// }
}
async function isRedeemedOnTerra(
terraContractData: TerraContractData,
vaaBytes: string
): Promise<boolean> {
let msg: Terra.MsgExecuteContract = null;
let sequenceNumber: number = 0;
try {
msg = new Terra.MsgExecuteContract(
terraContractData.wallet.key.accAddress,
terraContractData.tokenBridgeAddress,
{
submit_vaa: {
data: Buffer.from(vaaBytes, "hex").toString("base64"),
},
}
);
sequenceNumber = await terraContractData.wallet.sequence();
} catch (e) {
logger.error(
"isRedeemedOnTerra: failed to create message or look up sequence number, e: %o",
e
);
return false;
}
try {
await terraContractData.lcdClient.tx.estimateFee(
[{ sequenceNumber: sequenceNumber }],
{
msgs: [msg],
}
);
} catch (e) {
if (e.response.data.error.includes("VaaAlreadyExecuted")) {
return true;
}
logger.error(
"isRedeemedOnTerra: failed to check if transfer is already complete, e: %o",
e
);
}
return false;
}