Wormhole stub (#789)

* add

* add sei to wormhole chains

* add wormhole code to the repo

* add lib to gitignore

* cosmwasm bug fix

* add tmp to gitignore

* no need for simulation

* add wormhole deployment stuff

* better comments

* resolve build errors

* trying a fix

* fix

* rename compiled code

* address feedback

* remove gitignore

* sei deployment

* complete sentences

* address comments
This commit is contained in:
Dev Kalra 2023-05-03 20:48:00 +05:30 committed by GitHub
parent f94dceb1bc
commit 079828f8ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 3063 additions and 10488 deletions

View File

@ -14,6 +14,9 @@ export const RECEIVER_CHAINS = {
meter: 60010,
mantle: 60011,
conflux_espace: 60012,
sei: 60013,
osmosis: 60014,
neutron: 60015,
};
// If there is any overlapping value the receiver chain will replace the wormhole

3412
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"price_service/client/js",
"target_chains/aptos/sdk/js",
"target_chains/cosmwasm/sdk/js",
"target_chains/cosmwasm/tools",
"target_chains/ethereum/contracts",
"target_chains/ethereum/sdk/js",
"target_chains/ethereum/sdk/solidity",

View File

@ -51,7 +51,7 @@
"typescript": "^4.6.3"
},
"dependencies": {
"@injectivelabs/sdk-ts": "^1.0.484",
"@injectivelabs/sdk-ts": "1.10.72",
"@pythnetwork/price-service-client": "*",
"@pythnetwork/pyth-sdk-solidity": "*",
"@truffle/hdwallet-provider": "^2.1.3",

View File

@ -20,7 +20,7 @@ import {
createTransactionFromMsg,
} from "@injectivelabs/sdk-ts";
import { DEFAULT_GAS_PRICE } from "@injectivelabs/utils";
const DEFAULT_GAS_PRICE = 500000000;
type PriceQueryResponse = {
price_feed: {
@ -63,7 +63,7 @@ export class InjectivePriceListener extends ChainPriceListener {
Buffer.from(`{"price_feed":{"id":"${priceId}"}}`).toString("base64")
);
const json = Buffer.from(data as string, "base64").toString();
const json = Buffer.from(data).toString();
priceQueryResponse = JSON.parse(json);
} catch (e) {
console.error(`Polling on-chain price for ${priceId} failed. Error:`);
@ -163,8 +163,7 @@ export class InjectivePricePusher implements IPricePusher {
const sig = await this.wallet.sign(Buffer.from(signBytes));
/** Append Signatures */
txRaw.setSignaturesList([sig]);
txRaw.signatures = [sig];
const txResponse = await txService.broadcast(txRaw);
return txResponse;
@ -215,7 +214,7 @@ export class InjectivePricePusher implements IPricePusher {
).toString("base64")
);
const json = Buffer.from(data as string, "base64").toString();
const json = Buffer.from(data).toString();
updateFeeQueryResponse = JSON.parse(json);
} catch (e) {
console.error("Error fetching update fee");

View File

@ -1,2 +1,5 @@
artifacts/
lib
!bin
!wormhole-stub/artifacts

File diff suppressed because it is too large Load Diff

View File

@ -15,10 +15,10 @@
"@cosmjs/cosmwasm-stargate": "^0.29.5",
"@cosmjs/encoding": "^0.26.2",
"@cosmjs/proto-signing": "^0.30.1",
"@injectivelabs/networks": "^1.0.55",
"@injectivelabs/sdk-ts": "^1.0.354",
"@injectivelabs/utils": "^1.0.47",
"@injectivelabs/networks": "1.0.68",
"@injectivelabs/sdk-ts": "1.0.354",
"@ltd/j-toml": "^1.38.0",
"@pythnetwork/xc-governance-sdk": "*",
"@terra-money/terra.js": "^3.1.3",
"chain-registry": "^1.6.0",
"cosmjs-utils": "^0.1.0",

View File

@ -0,0 +1,120 @@
import { ChainExecutor } from "./chain-executor";
import { CosmwasmExecutor } from "./cosmwasm";
import { InjectiveExecutor } from "./injective";
export enum ChainType {
INJECTIVE = "injective",
COSMWASM = "cosmwasm",
}
// GUIDELINES: to add new chains
// ENUM Key should be of the form:
// CHAINNAME{_OPTIONAL-IDENTIFIER}
// ENUM Value should be of the form:
// chainname{_optional-identifier}
export enum ChainIdTestnet {
INJECTIVE = "injective",
OSMOSIS_4 = "osmosis_4",
OSMOSIS_5 = "osmosis_5",
SEI_ATLANTIC_2 = "sei_atlantic_2",
NEUTRON_PION_1 = "neutron_pion_1",
}
export const ChainIdsTestnet = Object.values(ChainIdTestnet);
// TODO: ADD MAINNET IDs IN FUTURE
// export enum ChainIdMainnet {
// INJECTIVE = "injective",
// }
export type ChainConfig =
| {
// usually the chain name
// osmosis, injective
chainId: ChainIdTestnet;
chainType: ChainType.INJECTIVE;
// endpoints to create executor and querier for a particular chain
querierEndpoint: string;
executorEndpoint: string;
}
| {
// usually the chain name
// osmosis, injective
chainId: ChainIdTestnet;
chainType: ChainType.COSMWASM;
// endpoints to create executor and querier for a particular chain
querierEndpoint: string;
executorEndpoint: string;
// some extra fields
// prefix of the particular cosmwasm chain
// eg "osmo"
prefix: string;
// gas price for that chain
// eg "0.025 uosmo"
gasPrice: string;
};
export const ChainsConfigTestnet: Record<ChainIdTestnet, ChainConfig> = {
[ChainIdTestnet.INJECTIVE]: {
chainId: ChainIdTestnet.INJECTIVE,
chainType: ChainType.INJECTIVE,
querierEndpoint: "https://k8s.testnet.tm.injective.network:443",
executorEndpoint: "https://k8s.testnet.chain.grpc-web.injective.network",
},
[ChainIdTestnet.OSMOSIS_5]: {
chainId: ChainIdTestnet.OSMOSIS_5,
chainType: ChainType.COSMWASM,
executorEndpoint: "https://rpc.osmotest5.osmosis.zone/",
querierEndpoint: "https://rpc.osmotest5.osmosis.zone/",
prefix: "osmo",
gasPrice: "0.025uosmo",
},
[ChainIdTestnet.OSMOSIS_4]: {
chainId: ChainIdTestnet.OSMOSIS_4,
chainType: ChainType.COSMWASM,
executorEndpoint: "https://rpc-test.osmosis.zone:443",
querierEndpoint: "https://rpc-test.osmosis.zone:443",
prefix: "osmo",
gasPrice: "0.025uosmo",
},
[ChainIdTestnet.SEI_ATLANTIC_2]: {
chainId: ChainIdTestnet.SEI_ATLANTIC_2,
chainType: ChainType.COSMWASM,
executorEndpoint: "https://rpc.atlantic-2.seinetwork.io/",
querierEndpoint: "https://rpc.atlantic-2.seinetwork.io/",
prefix: "sei",
gasPrice: "0.1usei",
},
[ChainIdTestnet.NEUTRON_PION_1]: {
chainId: ChainIdTestnet.NEUTRON_PION_1,
chainType: ChainType.COSMWASM,
executorEndpoint: "https://rpc.pion.rs-testnet.polypore.xyz/",
querierEndpoint: "https://rpc.pion.rs-testnet.polypore.xyz/",
prefix: "neutron",
gasPrice: "0.025untrn",
},
};
/**
* This method will return an executor for that corresponding chainType for given chainId.
*/
export function createExecutorForChain(
chainId: ChainIdTestnet,
mnemonic: string
): ChainExecutor {
const chainConfig = ChainsConfigTestnet[chainId];
const chainType = chainConfig.chainType;
if (chainType === ChainType.INJECTIVE) {
return new InjectiveExecutor(chainConfig.executorEndpoint, mnemonic);
} else
return new CosmwasmExecutor(
chainConfig.executorEndpoint,
mnemonic,
chainConfig.prefix,
chainConfig.gasPrice
);
}

View File

@ -63,16 +63,10 @@ export class CosmwasmExecutor implements ChainExecutor {
}
);
const gasUsed = await cosmwasmClient.simulate(
address,
[encodedMsgObject],
"auto"
);
const txResponse = await cosmwasmClient.signAndBroadcast(
address,
[encodedMsgObject],
calculateFee(gasUsed * 1.5, this.gasPrice)
1.5
);
if (txResponse.code !== 0) {

View File

@ -24,9 +24,10 @@ import {
UpdateContractAdminRequest,
UpdateContractAdminResponse,
} from "./chain-executor";
import { DEFAULT_GAS_PRICE } from "@injectivelabs/utils";
import assert from "assert";
const DEFAULT_GAS_PRICE = 500000000;
export class InjectiveExecutor implements ChainExecutor {
private readonly wallet: PrivateKey;
private readonly chainId = "injective-888";
@ -131,6 +132,7 @@ export class InjectiveExecutor implements ChainExecutor {
admin: this.getAddress(),
codeId,
label,
// @ts-ignore: bug in the injective's sdk
msg: instMsg,
});

View File

@ -35,6 +35,12 @@ export const CONFIG: Config = {
name: "localterra",
},
},
[NETWORKS.INJECTIVE_MAINNET]: {
type: CONFIG_TYPE.INJECTIVE,
host: {
network: Network.Mainnet,
},
},
[NETWORKS.INJECTIVE_TESTNET]: {
type: CONFIG_TYPE.INJECTIVE,
host: {

View File

@ -120,6 +120,9 @@ export class OsmosisDeployer implements Deployer {
cosmwasm.wasm.v1.MessageComposer.withTypeUrl.instantiateContract({
sender: accAddress,
admin: accAddress,
// FIXME: soon this file will be removed
// not spending any time on this bug
// @ts-ignore
codeId: Long.fromNumber(codeId),
label,
msg: Buffer.from(JSON.stringify(inst_msg)),
@ -155,6 +158,7 @@ export class OsmosisDeployer implements Deployer {
cosmwasm.wasm.v1.MessageComposer.withTypeUrl.migrateContract({
sender: await this.getAccountAddress(),
contract,
// @ts-ignore
codeId: Long.fromNumber(codeId),
msg: Buffer.from(
JSON.stringify({

View File

@ -0,0 +1,175 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { createInterface } from "readline";
import path from "path";
// This function lets you write a question to the terminal
// And returns the response of the user
function readLineAsync(msg: string) {
const readline = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
readline.question(msg, (userRes) => {
resolve(userRes);
});
});
}
// The stage executor is where the stage functionality is defined
// Optionally it can take in a method `getResultOfPastStage` as a parameter
// if it wants to access the result of the previous stages
// Each stage should have exactly one atomic operation (like sending a transaction),
// The pipeline doesn't enforce atomicity. So if you have 2 atomic operations in
// one stage, then you could end up fulfilling one and failing the other.
export type StageExecutor =
| ((
// get the result of a past stage using it's id
// It will return the result for the same step id
// It will return undefined if the previous stage data has not been stored locally
// or if a future stage data is being asked
getResultOfPastStage: <Y>(stageId: string) => Y
) => Promise<any>)
| (() => Promise<any>);
export type Stage = {
id: string;
executor: StageExecutor;
};
type StageResult<T = any> =
| {
status: "rejected";
reason: any;
}
| {
status: "fulfilled";
result: T;
};
export class Pipeline {
private stages: Stage[] = [];
private readonly pipelineStore: PipelineStore;
constructor(
// osmosis_testnet_4
private readonly id: string,
readonly storageFilePath: string
) {
this.pipelineStore = new PipelineStore(storageFilePath);
}
addStage(stage: Stage) {
this.stages.push(stage);
}
private stageExecutorWrapper(executor: StageExecutor) {
// We want to wrap the executor provided by the pipeline consumer
// In order to wrap the response of the executor in the StageResult
// also in this method we inject the `getResultOfPastStage` to the stage executor
return async (): Promise<StageResult> => {
// method to inject
const getResultOfPastStage = <Y>(stageId: string): Y => {
let result = this.pipelineStore.getStageState<StageResult<Y>>(stageId);
// This if condition will execute only if the stage executor is
// trying to reading a stage's state with stage id that doesn't exist
// past results will all be fulfilled and the pipeline will make sure of that
if (
result === undefined ||
(result !== undefined && result.status === "rejected")
) {
throw new Error(
`${this.id}: Stage id seems to be invalid: ${stageId}`
);
}
return result.result;
};
try {
// wrapping result
const result = await executor(getResultOfPastStage);
return {
status: "fulfilled",
result,
};
} catch (e) {
return {
status: "rejected",
reason: e,
};
}
};
}
private async processStage(stage: Stage): Promise<boolean> {
// Here we will check if there is a past result that has been fulfilled
// If yes, we are not going to process any further
let currentResult = this.pipelineStore.getStageState<StageResult>(stage.id);
if (currentResult !== undefined && currentResult.status === "fulfilled")
return true;
// Else we will process the new stage and store the result
const newResult = await this.stageExecutorWrapper(stage.executor)();
this.pipelineStore.setStageState(stage.id, newResult);
if (newResult.status === "fulfilled") return true;
console.log(`${this.id}: Stage with id: ${stage.id} failed.`);
console.log(`Please fix the error and re run the pipeline`);
return false;
}
async run() {
console.log("Running pipeline with id: ", this.id);
for (let stage of this.stages) {
console.log(`${this.id}: Running stage with id: ${stage.id}`);
// This method is only going to process stage if all the past ones have been fulfilled
let fulfilled = await this.processStage(stage);
// store the whole processing locally after every stage
this.pipelineStore.commit();
if (fulfilled === false) break;
}
}
}
// PipelineStore helps in getting and setting the state locally
// It manipulates data in-memory and once the consumer has finished manipulating it
// They need to commit the data to permanent storage using the commit method
class PipelineStore {
private readonly store: {
[stageId: string]: any;
};
constructor(private readonly filePath: string) {
if (!existsSync(this.filePath)) {
this.store = {};
return;
}
this.store = JSON.parse(readFileSync(this.filePath).toString());
}
// It gets the latest state for the given stage
// the state after the last operation
// if there is no stage state, in case it was no process it will return undefined.
// the caller can provide the T and this method will cast the result into it.
getStageState<T = any>(stageId: string): T | undefined {
return this.store[stageId];
}
// It sets the latest state for the given step
setStageState(stageId: string, state: any) {
this.store[stageId] = state;
}
// After all the in memory operations one can commit to the local file
// for permanent storage
commit() {
mkdirSync(path.dirname(this.filePath), { recursive: true });
writeFileSync(this.filePath, JSON.stringify(this.store, null, 4));
}
}

View File

@ -0,0 +1,150 @@
import {
ChainIdTestnet,
ChainIdsTestnet,
createExecutorForChain,
} from "./chains-manager/chains";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { readFileSync } from "fs";
import {
InstantiateContractResponse,
StoreCodeResponse,
} from "./chains-manager/chain-executor";
import { Pipeline } from "./pipeline";
import { CHAINS } from "@pythnetwork/xc-governance-sdk";
const argv = yargs(hideBin(process.argv))
.usage("USAGE: npm run wormhole-stub -- <command>")
.option("mnemonic", {
type: "string",
demandOption: "Please provide the mnemonic",
})
.option("contract-version", {
type: "string",
demandOption: `Please input the contract-version of the wormhole contract.
There should be a compiled code at the path - "../wormhole-stub/artifacts/wormhole-\${contract-version}.wasm"`,
})
.option("chain-id", {
type: "string",
choices: ChainIdsTestnet,
})
.option("mainnet", {
type: "boolean",
desc: "Execute this script for mainnet networks. THIS WILL BE ADDED IN FUTURE",
})
.help()
.alias("help", "h")
.wrap(yargs.terminalWidth())
.parseSync();
// IMPORTANT: IN ORDER TO RUN THIS SCRIPT FOR CHAINS
// WE NEED SOME METADATA
// HERE IS WHERE WE WILL BE ADDING THAT
// The type definition here make sure that the chain is added to xc_governance_sdk_js before this script was executed
type WormholeConfig = Record<
ChainIdTestnet,
{ feeDenom: string; chainId: number }
>;
const wormholeConfig: WormholeConfig = {
[ChainIdTestnet.INJECTIVE]: {
feeDenom: "inj",
chainId: CHAINS.injective,
},
[ChainIdTestnet.OSMOSIS_4]: {
feeDenom: "uosmo",
chainId: CHAINS.osmosis,
},
[ChainIdTestnet.OSMOSIS_5]: {
feeDenom: "uosmo",
chainId: CHAINS.osmosis,
},
[ChainIdTestnet.SEI_ATLANTIC_2]: {
feeDenom: "usei",
chainId: CHAINS.sei,
},
[ChainIdTestnet.NEUTRON_PION_1]: {
feeDenom: "untrn",
chainId: CHAINS.neutron,
},
};
async function run() {
const STORAGE_DIR = "../wormhole-stub/testnet";
let wasmFilePath = `../wormhole-stub/artifacts/wormhole-${argv.contractVersion}.wasm`;
// get the wormhole code
const contractBytes = readFileSync(wasmFilePath);
let chainIds = argv.chainId === undefined ? ChainIdsTestnet : [argv.chainId];
for (let chainId of chainIds) {
let pipelineStoreFilePath = `${STORAGE_DIR}/${chainId}-${argv.contractVersion}.json`;
const pipeline = new Pipeline(chainId, pipelineStoreFilePath);
const chainExecutor = createExecutorForChain(chainId, argv.mnemonic);
// add stages
// 1 deploy artifact
pipeline.addStage({
id: "deploy-wormhole-code",
executor: async () => {
return chainExecutor.storeCode({
contractBytes,
});
},
});
// 2 instantiate contract
pipeline.addStage({
id: "instantiate-contract",
executor: (getResultOfPastStage) => {
const storeCodeRes: StoreCodeResponse = getResultOfPastStage(
"deploy-wormhole-code"
);
return chainExecutor.instantiateContract({
codeId: storeCodeRes.codeId,
instMsg: getWormholeConfig(wormholeConfig[chainId]),
label: "wormhole",
});
},
});
// 3 set its own admin
pipeline.addStage({
id: "set-own-admin",
executor: (getResultOfPastStage) => {
const instantiateContractRes: InstantiateContractResponse =
getResultOfPastStage("instantiate-contract");
return chainExecutor.updateContractAdmin({
newAdminAddr: instantiateContractRes.contractAddr,
contractAddr: instantiateContractRes.contractAddr,
});
},
});
await pipeline.run();
}
}
function getWormholeConfig({
feeDenom,
chainId,
}: {
feeDenom: string;
chainId: number;
}) {
return {
chain_id: chainId,
fee_denom: feeDenom,
gov_chain: 1,
gov_address: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ=",
guardian_set_expirity: 86400,
initial_guardian_set: {
addresses: [{ bytes: "WMw65cCXshPOPIGXnhuflXB0aqU=" }],
expiration_time: 0,
},
};
}
run();

View File

@ -24,4 +24,4 @@ WORKDIR /code
RUN --mount=type=cache,target=/code/target,id=cosmwasm_wormhole_target --mount=type=cache,target=/usr/local/cargo/registry optimize_workspace.sh
FROM scratch AS export-stage
COPY --from=builder /code/artifacts/wormhole* /
COPY --from=builder /code/artifacts/ /

View File

@ -0,0 +1,22 @@
{
"deploy-wormhole-code": {
"status": "fulfilled",
"result": {
"txHash": "609E6EFFD1C191FA69157BC124CD15B0448E30A11AA65D36CF8BE66666127828",
"codeId": 1116
}
},
"instantiate-contract": {
"status": "fulfilled",
"result": {
"txHash": "6408D6691CF40E3C13062F12FD65E3599722A8FDBD3664852BB44DC739C57B9A",
"contractAddr": "inj1ks8v2tvx2vsqxx7sgckl9h7rxga60tuvgezpps"
}
},
"set-own-admin": {
"status": "fulfilled",
"result": {
"txHash": "5B31A48AEF4250FAA09117EF63E49E81B6EFBFAC1B1C6AEFF80265434A70DD7D"
}
}
}

View File

@ -0,0 +1,22 @@
{
"deploy-wormhole-code": {
"status": "fulfilled",
"result": {
"codeId": 244,
"txHash": "C8F04CFACC75C28862F3E2A6D08F5F9B869268F09B9D792EBD6C4EC8BC8F36DE"
}
},
"instantiate-contract": {
"status": "fulfilled",
"result": {
"contractAddr": "neutron17xlvf3f82tklvzpveam56n96520pdrxfgpralyhf3nq7f33uvgzqrgegc7",
"txHash": "8456CC89F7253998E0A22AD5F037574682315F332C661AC357AF927FDFF489B0"
}
},
"set-own-admin": {
"status": "fulfilled",
"result": {
"txHash": "B9188160725B5282D11DB83F1AC28F88879A4C03AF7B6F178EA44BD07BB76805"
}
}
}

View File

@ -0,0 +1,22 @@
{
"deploy-wormhole-code": {
"status": "fulfilled",
"result": {
"codeId": 6949,
"txHash": "504970A0B21933FCE9BA22E2819AEEC643E7DBFA230C7BEA2E8A068522605E65"
}
},
"instantiate-contract": {
"status": "fulfilled",
"result": {
"contractAddr": "osmo18njur8dzzq6lm5dd6n2td94jgmnywt0j9es2ymxpa0zyy7jrwwuq4v8arc",
"txHash": "E8B11F851BF79448129D1F75D67B9020A794E9DA9686B56EDC458FFC338D69B1"
}
},
"set-own-admin": {
"status": "fulfilled",
"result": {
"txHash": "CACD7F3608E915AC629B399C63EE2C595583AE76418949847B43795208F656EE"
}
}
}

View File

@ -0,0 +1,22 @@
{
"deploy-wormhole-code": {
"status": "fulfilled",
"result": {
"codeId": 58,
"txHash": "48375A885AE7047D38C4001C4F16F8852E4971F7E761108075AC4BD447A05AFB"
}
},
"instantiate-contract": {
"status": "fulfilled",
"result": {
"contractAddr": "osmo1224ksv5ckfcuz2geeqfpdu2u3uf706y5fx8frtgz6egmgy0hkxxqtgad95",
"txHash": "B5A7FDC5220C05D446BBB9207F0DDA8C8C7FEB1DBF11127993D014C20FAF0CFA"
}
},
"set-own-admin": {
"status": "fulfilled",
"result": {
"txHash": "E4CA160348165532DBE7B43C116A801E78FE77122928DB9F9D4DB41514F53F53"
}
}
}

View File

@ -0,0 +1,22 @@
{
"deploy-wormhole-code": {
"status": "fulfilled",
"result": {
"codeId": 369,
"txHash": "240C71EE74A1C676A054FD6F6E81CC46701103EF8CAB3A431ACFC18042B0AC09"
}
},
"instantiate-contract": {
"status": "fulfilled",
"result": {
"contractAddr": "sei1tu7w5lxsckpa4ahd4umra0k02zyd7eq79j7zxk8e3ds8evlejywqrtsl6a",
"txHash": "72FA96116275D66D8CF83BCD955E837FDB9CBA085CF0D57EC6FA30C20F106E0B"
}
},
"set-own-admin": {
"status": "fulfilled",
"result": {
"txHash": "3BFF9026BCAF0B08C1A6FDE3F26D051860D21CCCC1DF28D9A09D0C9F570815A0"
}
}
}