[xc-admin] Contract management tool (#885)
* cleanup * blah * gr * stuff * hm * hmm * wtf * ah fix this * ok finally it does something * ok * hrm * hrm * blah * blah
This commit is contained in:
parent
e1377e5627
commit
25c1ac2c33
|
@ -16,7 +16,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write \"src/**/*.ts\""
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"cli": "npm run build && node lib/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.26.0",
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import { program } from "commander";
|
||||
import { loadContractConfig, ContractType, SyncOp } from "xc_admin_common";
|
||||
import * as fs from "fs";
|
||||
|
||||
// TODO: extract this configuration to a file
|
||||
const contractsConfig = [
|
||||
{
|
||||
type: ContractType.EvmPythUpgradable,
|
||||
networkId: "arbitrum",
|
||||
address: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
|
||||
},
|
||||
{
|
||||
type: ContractType.EvmWormholeReceiver,
|
||||
networkId: "canto",
|
||||
address: "0x87047526937246727E4869C5f76A347160e08672",
|
||||
},
|
||||
{
|
||||
type: ContractType.EvmPythUpgradable,
|
||||
networkId: "canto",
|
||||
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
|
||||
},
|
||||
{
|
||||
type: ContractType.EvmPythUpgradable,
|
||||
networkId: "avalanche",
|
||||
address: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
|
||||
},
|
||||
];
|
||||
|
||||
const networksConfig = {
|
||||
evm: {
|
||||
optimism_goerli: {
|
||||
url: `https://rpc.ankr.com/optimism_testnet`,
|
||||
},
|
||||
arbitrum: {
|
||||
url: "https://arb1.arbitrum.io/rpc",
|
||||
},
|
||||
avalanche: {
|
||||
url: "https://api.avax.network/ext/bc/C/rpc",
|
||||
},
|
||||
canto: {
|
||||
url: "https://canto.gravitychain.io",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: we will need configuration of this stuff to decide which multisig to run.
|
||||
const multisigs = [
|
||||
{
|
||||
name: "",
|
||||
wormholeNetwork: "mainnet",
|
||||
},
|
||||
];
|
||||
|
||||
program
|
||||
.name("pyth_governance")
|
||||
.description("CLI for governing Pyth contracts")
|
||||
.version("0.1.0");
|
||||
|
||||
program
|
||||
.command("get")
|
||||
.description("Find Pyth contracts matching the given search criteria")
|
||||
.option("-n, --network <network-id>", "Find contracts on the given network")
|
||||
.option("-a, --address <address>", "Find contracts with the given address")
|
||||
.option("-t, --type <type-id>", "Find contracts of the given type")
|
||||
.action(async (options: any) => {
|
||||
const contracts = loadContractConfig(contractsConfig, networksConfig);
|
||||
|
||||
console.log(JSON.stringify(options));
|
||||
|
||||
const matches = [];
|
||||
for (const contract of contracts) {
|
||||
if (
|
||||
(options.network === undefined ||
|
||||
contract.networkId == options.network) &&
|
||||
(options.address === undefined ||
|
||||
contract.getAddress() == options.address) &&
|
||||
(options.type === undefined || contract.type == options.type)
|
||||
) {
|
||||
matches.push(contract);
|
||||
}
|
||||
}
|
||||
|
||||
for (const contract of matches) {
|
||||
const state = await contract.getState();
|
||||
console.log({
|
||||
networkId: contract.networkId,
|
||||
address: contract.getAddress(),
|
||||
type: contract.type,
|
||||
state: state,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
class Cache {
|
||||
private path: string;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
private opFilePath(op: SyncOp): string {
|
||||
return `${this.path}/${op.id()}.json`;
|
||||
}
|
||||
|
||||
public readOpCache(op: SyncOp): Record<string, any> {
|
||||
const path = this.opFilePath(op);
|
||||
if (fs.existsSync(path)) {
|
||||
return JSON.parse(fs.readFileSync(path).toString("utf-8"));
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public writeOpCache(op: SyncOp, cache: Record<string, any>) {
|
||||
fs.writeFileSync(this.opFilePath(op), JSON.stringify(cache));
|
||||
}
|
||||
|
||||
public deleteCache(op: SyncOp) {
|
||||
fs.rmSync(this.opFilePath(op));
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command("set")
|
||||
.description("Set a configuration parameter for one or more Pyth contracts")
|
||||
.option("-n, --network <network-id>", "Find contracts on the given network")
|
||||
.option("-a, --address <address>", "Find contracts with the given address")
|
||||
.option("-t, --type <type-id>", "Find contracts of the given type")
|
||||
.argument("<fields...>", "Fields to set on the given contracts")
|
||||
.action(async (fields, options: any, command) => {
|
||||
const contracts = loadContractConfig(contractsConfig, networksConfig);
|
||||
|
||||
console.log(JSON.stringify(fields));
|
||||
console.log(JSON.stringify(options));
|
||||
|
||||
const setters = fields.map((value: string) => value.split("="));
|
||||
|
||||
const matches = [];
|
||||
for (const contract of contracts) {
|
||||
if (
|
||||
(options.network === undefined ||
|
||||
contract.networkId == options.network) &&
|
||||
(options.address === undefined ||
|
||||
contract.getAddress() == options.address) &&
|
||||
(options.type === undefined || contract.type == options.type)
|
||||
) {
|
||||
matches.push(contract);
|
||||
}
|
||||
}
|
||||
|
||||
const ops = [];
|
||||
for (const contract of matches) {
|
||||
const state = await contract.getState();
|
||||
// TODO: make a decent format for this
|
||||
for (const [field, value] of setters) {
|
||||
state[field] = value;
|
||||
}
|
||||
|
||||
ops.push(...(await contract.sync(state)));
|
||||
}
|
||||
|
||||
// TODO: extract constant
|
||||
const cacheDir = "cache";
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
const cache = new Cache(cacheDir);
|
||||
|
||||
for (const op of ops) {
|
||||
const opCache = cache.readOpCache(op);
|
||||
const isDone = await op.run(opCache);
|
||||
if (isDone) {
|
||||
cache.deleteCache(op);
|
||||
} else {
|
||||
cache.writeOpCache(op, opCache);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
|
@ -23,9 +23,12 @@
|
|||
"@certusone/wormhole-sdk": "^0.9.8",
|
||||
"@coral-xyz/anchor": "^0.26.0",
|
||||
"@pythnetwork/client": "^2.17.0",
|
||||
"@pythnetwork/pyth-sdk-solidity": "*",
|
||||
"@pythnetwork/xc-governance-sdk": "*",
|
||||
"@solana/buffer-layout": "^4.0.1",
|
||||
"@solana/web3.js": "^1.73.0",
|
||||
"@sqds/mesh": "^1.0.6",
|
||||
"ethers": "^5.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import { ChainId, Instruction } from "@pythnetwork/xc-governance-sdk";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
export enum ContractType {
|
||||
Oracle,
|
||||
EvmPythUpgradable,
|
||||
EvmWormholeReceiver,
|
||||
}
|
||||
|
||||
/**
|
||||
* A unique identifier for a blockchain. Note that we cannot use ChainId for this, as ChainId currently reuses
|
||||
* some ids across mainnet / testnet chains (e.g., ethereum goerli has the same id as ethereum mainnet).
|
||||
*/
|
||||
export type NetworkId = string;
|
||||
|
||||
/** A unique identifier for message senders across all wormhole networks. */
|
||||
export interface WormholeAddress {
|
||||
emitter: string;
|
||||
chainId: ChainId;
|
||||
// which network this sender is on
|
||||
network: WormholeNetwork;
|
||||
}
|
||||
export type WormholeNetwork = "mainnet" | "testnet";
|
||||
|
||||
/**
|
||||
* A Contract is the basic unit of on-chain state that is managed by xc_admin.
|
||||
* Each contracts lives at a specific address of a specific network, and has a type
|
||||
* representing which of several known contract types (evm target chain, wormhole receiver, etc)
|
||||
* that it is.
|
||||
*
|
||||
* Contracts further expose a state representing values that can be modified by governance.
|
||||
* The fields of the state object vary depending on what type of contract this is.
|
||||
* Finally, contracts expose a sync method that generates the needed operations to bring the on-chain state
|
||||
* in sync with a provided desired state.
|
||||
*/
|
||||
export interface Contract<State> {
|
||||
type: ContractType;
|
||||
networkId: NetworkId;
|
||||
/** The address of the contract. The address may be written in different formats for different networks. */
|
||||
getAddress(): string;
|
||||
|
||||
/** Get the on-chain state of all governance-controllable fields of this contract. */
|
||||
getState(): Promise<State>;
|
||||
|
||||
/** Generate a set of operations that, if executed, will update the on-chain contract state to be `target`. */
|
||||
sync(target: State): Promise<SyncOp[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An idempotent synchronization operation to update on-chain state. The operation may depend on
|
||||
* external approvals or actions to complete, in which case the operation will pause and need to
|
||||
* be resumed later.
|
||||
*/
|
||||
export interface SyncOp {
|
||||
/**
|
||||
* A unique identifier for this operation. The id represents the content of the operation (e.g., "sets the X
|
||||
* field to Y on contract Z"), so can be used to identify the "same" operation across multiple runs of this program.
|
||||
*/
|
||||
id(): string;
|
||||
/**
|
||||
* Run this operation from a previous state (recorded in cache). The operation can modify cache
|
||||
* to record progress, then returns true if the operation has completed. If this function returns false,
|
||||
* it is waiting on an external operation to complete (e.g., a multisig transaction to be approved).
|
||||
* Re-run this function again once that operation is completed to continue making progress.
|
||||
*
|
||||
* The caller of this function is responsible for preserving the contents of `cache` between calls to
|
||||
* this function.
|
||||
*/
|
||||
run(cache: Record<string, any>): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class SendGovernanceInstruction implements SyncOp {
|
||||
private instruction: Instruction;
|
||||
private sender: WormholeAddress;
|
||||
// function to submit the signed VAA to the target chain contract
|
||||
private submitVaa: (vaa: string) => Promise<boolean>;
|
||||
|
||||
constructor(
|
||||
instruction: Instruction,
|
||||
from: WormholeAddress,
|
||||
submitVaa: (vaa: string) => Promise<boolean>
|
||||
) {
|
||||
this.instruction = instruction;
|
||||
this.sender = from;
|
||||
this.submitVaa = submitVaa;
|
||||
}
|
||||
|
||||
public id(): string {
|
||||
// TODO: use a more understandable identifier (also this may not be unique)
|
||||
return ethers.utils.sha256(this.instruction.serialize());
|
||||
}
|
||||
|
||||
public async run(cache: Record<string, any>): Promise<boolean> {
|
||||
// FIXME: this implementation is temporary. replace with something like the commented out code below.
|
||||
if (cache["multisigTx"] === undefined) {
|
||||
cache["multisigTx"] = "fooooo";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache["vaa"] === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// VAA is guaranteed to be defined here
|
||||
const vaa = cache["vaa"];
|
||||
|
||||
// assertVaaPayloadEquals(vaa, payload);
|
||||
|
||||
return await this.submitVaa(vaa);
|
||||
}
|
||||
|
||||
/*
|
||||
public async run(cache: Record<string,any>): Promise<boolean> {
|
||||
if (cache["multisigTx"] === undefined) {
|
||||
// Have not yet submitted this operation to the multisig.
|
||||
const payload = this.instruction.serialize();
|
||||
const txKey = vault.sendWormholeInstruction(payload);
|
||||
cache["multisigTx"] = txKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache["vaa"] === undefined) {
|
||||
const vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex);
|
||||
if (vaa === undefined) {
|
||||
return false;
|
||||
}
|
||||
cache["vaa"] = vaa;
|
||||
}
|
||||
|
||||
// VAA is guaranteed to be defined here
|
||||
const vaa = cache["vaa"];
|
||||
|
||||
assertVaaPayloadEquals(vaa, payload);
|
||||
|
||||
// await proxy.executeGovernanceInstruction("0x" + vaa);
|
||||
await submitVaa(vaa);
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import {
|
||||
Contract,
|
||||
ContractType,
|
||||
NetworkId,
|
||||
SendGovernanceInstruction,
|
||||
SyncOp,
|
||||
WormholeAddress,
|
||||
WormholeNetwork,
|
||||
} from "./Contract";
|
||||
import {
|
||||
ChainId,
|
||||
SetValidPeriodInstruction,
|
||||
} from "@pythnetwork/xc-governance-sdk";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
export class EvmPythUpgradable implements Contract<EvmPythUpgradableState> {
|
||||
public type = ContractType.EvmPythUpgradable;
|
||||
public networkId;
|
||||
private address;
|
||||
|
||||
private contract: ethers.Contract;
|
||||
|
||||
constructor(
|
||||
networkId: NetworkId,
|
||||
address: string,
|
||||
contract: ethers.Contract
|
||||
) {
|
||||
this.networkId = networkId;
|
||||
this.address = address;
|
||||
this.contract = contract;
|
||||
}
|
||||
|
||||
public getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
// TODO: these getters will need the full PythUpgradable ABI
|
||||
public async getAuthority(): Promise<WormholeAddress> {
|
||||
// FIXME: read from data sources
|
||||
return {
|
||||
emitter: "123454",
|
||||
chainId: 1,
|
||||
network: "mainnet",
|
||||
};
|
||||
}
|
||||
|
||||
// get the chainId that identifies this contract
|
||||
public async getChainId(): Promise<ChainId> {
|
||||
// FIXME: read from data sources
|
||||
return 23;
|
||||
}
|
||||
|
||||
public async getState(): Promise<EvmPythUpgradableState> {
|
||||
const bytecodeSha = ethers.utils.sha256(
|
||||
(await this.contract.provider.getCode(this.contract.address)) as string
|
||||
);
|
||||
const validTimePeriod =
|
||||
(await this.contract.getValidTimePeriod()) as bigint;
|
||||
return {
|
||||
bytecodeSha,
|
||||
validTimePeriod: validTimePeriod.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
public async sync(target: EvmPythUpgradableState): Promise<SyncOp[]> {
|
||||
const myState = await this.getState();
|
||||
const authority = await this.getAuthority();
|
||||
const myChainId = await this.getChainId();
|
||||
const whInstructions = [];
|
||||
|
||||
if (myState.validTimePeriod !== target.validTimePeriod) {
|
||||
whInstructions.push(
|
||||
new SetValidPeriodInstruction(myChainId, BigInt(target.validTimePeriod))
|
||||
);
|
||||
}
|
||||
|
||||
return whInstructions.map(
|
||||
(value) =>
|
||||
new SendGovernanceInstruction(
|
||||
value,
|
||||
authority,
|
||||
this.submitGovernanceVaa
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async submitGovernanceVaa(vaa: string): Promise<boolean> {
|
||||
// FIXME: also needs the full PythUpgradable ABI
|
||||
// await this.contract.executeGovernanceInstruction("0x" + vaa)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EvmPythUpgradableState {
|
||||
bytecodeSha: string;
|
||||
// bigint serialized as a string
|
||||
validTimePeriod: string;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { ethers } from "ethers";
|
||||
import { Contract, ContractType, NetworkId, SyncOp } from "./Contract";
|
||||
|
||||
export class EvmWormholeReceiver implements Contract<EvmWormholeReceiverState> {
|
||||
public type = ContractType.EvmWormholeReceiver;
|
||||
public networkId;
|
||||
private address;
|
||||
|
||||
private contract: ethers.Contract;
|
||||
|
||||
constructor(
|
||||
networkId: NetworkId,
|
||||
address: string,
|
||||
contract: ethers.Contract
|
||||
) {
|
||||
this.networkId = networkId;
|
||||
this.address = address;
|
||||
this.contract = contract;
|
||||
}
|
||||
|
||||
public getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public async getState(): Promise<EvmWormholeReceiverState> {
|
||||
const bytecodeSha = ethers.utils.sha256(
|
||||
(await this.contract.provider.getCode(this.contract.address)) as string
|
||||
);
|
||||
|
||||
return {
|
||||
bytecodeSha,
|
||||
};
|
||||
}
|
||||
|
||||
public async sync(target: EvmWormholeReceiverState): Promise<SyncOp[]> {
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export interface EvmWormholeReceiverState {
|
||||
bytecodeSha: string;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { ethers } from "ethers";
|
||||
import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
|
||||
import { Contract, ContractType, NetworkId } from "./Contract";
|
||||
import { EvmPythUpgradable } from "./EvmPythUpgradable";
|
||||
import { EvmWormholeReceiver } from "./EvmWormholeReceiver";
|
||||
|
||||
export function getEvmProvider(
|
||||
networkId: NetworkId,
|
||||
networksConfig: any
|
||||
): ethers.providers.Provider {
|
||||
const networkConfig = networksConfig["evm"][networkId]!;
|
||||
return ethers.getDefaultProvider(networkConfig.url);
|
||||
}
|
||||
|
||||
export function loadContractConfig(
|
||||
contractsConfig: any,
|
||||
networksConfig: any
|
||||
): Contract<any>[] {
|
||||
const contracts = [];
|
||||
for (const contractConfig of contractsConfig) {
|
||||
contracts.push(fromConfig(contractConfig, networksConfig));
|
||||
}
|
||||
return contracts;
|
||||
}
|
||||
|
||||
function fromConfig(contractConfig: any, networksConfig: any): Contract<any> {
|
||||
switch (contractConfig.type) {
|
||||
case ContractType.EvmPythUpgradable: {
|
||||
const ethersContract = new ethers.Contract(
|
||||
contractConfig.address,
|
||||
PythAbi,
|
||||
getEvmProvider(contractConfig.networkId, networksConfig)
|
||||
);
|
||||
|
||||
return new EvmPythUpgradable(
|
||||
contractConfig.networkId,
|
||||
contractConfig.address,
|
||||
ethersContract
|
||||
);
|
||||
}
|
||||
case ContractType.EvmWormholeReceiver: {
|
||||
const ethersContract = new ethers.Contract(
|
||||
contractConfig.address,
|
||||
// TODO: pass in an appropriate ABI here
|
||||
[],
|
||||
getEvmProvider(contractConfig.networkId, networksConfig)
|
||||
);
|
||||
|
||||
return new EvmWormholeReceiver(
|
||||
contractConfig.networkId,
|
||||
contractConfig.address,
|
||||
ethersContract
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new Error(`unknown contract type: ${contractConfig.type}`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./config";
|
||||
export * from "./Contract";
|
||||
export * from "./EvmPythUpgradable";
|
||||
export * from "./EvmWormholeReceiver";
|
|
@ -9,3 +9,4 @@ export * from "./bpf_upgradable_loader";
|
|||
export * from "./deterministic_oracle_accounts";
|
||||
export * from "./cranks";
|
||||
export * from "./message_buffer";
|
||||
export * from "./contracts";
|
||||
|
|
|
@ -5,6 +5,7 @@ const nextConfig = {
|
|||
externalDir: true,
|
||||
},
|
||||
webpack(config) {
|
||||
config.experiments = { asyncWebAssembly: true }
|
||||
config.resolve.fallback = { fs: false }
|
||||
const fileLoaderRule = config.module.rules.find(
|
||||
(rule) => rule.test && rule.test.test('.svg')
|
||||
|
|
|
@ -1335,9 +1335,11 @@
|
|||
"@certusone/wormhole-sdk": "^0.9.8",
|
||||
"@coral-xyz/anchor": "^0.26.0",
|
||||
"@pythnetwork/client": "^2.17.0",
|
||||
"@pythnetwork/pyth-sdk-solidity": "*",
|
||||
"@solana/buffer-layout": "^4.0.1",
|
||||
"@solana/web3.js": "^1.73.0",
|
||||
"@sqds/mesh": "^1.0.6",
|
||||
"ethers": "^5.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
|
@ -106547,12 +106549,14 @@
|
|||
"@certusone/wormhole-sdk": "^0.9.8",
|
||||
"@coral-xyz/anchor": "^0.26.0",
|
||||
"@pythnetwork/client": "^2.17.0",
|
||||
"@pythnetwork/pyth-sdk-solidity": "*",
|
||||
"@solana/buffer-layout": "^4.0.1",
|
||||
"@solana/web3.js": "^1.73.0",
|
||||
"@sqds/mesh": "^1.0.6",
|
||||
"@types/bn.js": "^5.1.1",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"ethers": "^5.7.2",
|
||||
"jest": "^29.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^2.8.1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { readFileSync } from "fs";
|
||||
import { Bech32, toHex } from "@cosmjs/encoding";
|
||||
import { zeroPad } from "ethers/lib/utils.js";
|
||||
import { ethers } from "ethers";
|
||||
import assert from "assert";
|
||||
import { getNetworkInfo, Network } from "@injectivelabs/networks";
|
||||
import {
|
||||
|
@ -210,7 +210,7 @@ export class InjectiveDeployer implements Deployer {
|
|||
// Injective addresses are "human-readable", but for cross-chain registrations, we
|
||||
// want the "canonical" version
|
||||
function convert_injective_address_to_hex(human_addr: string) {
|
||||
return "0x" + toHex(zeroPad(Bech32.decode(human_addr).data, 32));
|
||||
return "0x" + toHex(ethers.utils.zeroPad(Bech32.decode(human_addr).data, 32));
|
||||
}
|
||||
|
||||
// enter key of what to extract
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from "@terra-money/terra.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { Bech32, toHex } from "@cosmjs/encoding";
|
||||
import { zeroPad } from "ethers/lib/utils.js";
|
||||
import { ethers } from "ethers";
|
||||
import assert from "assert";
|
||||
import { ContractInfo, Deployer } from ".";
|
||||
|
||||
|
@ -179,7 +179,7 @@ export class TerraDeployer implements Deployer {
|
|||
// Terra addresses are "human-readable", but for cross-chain registrations, we
|
||||
// want the "canonical" version
|
||||
export function convert_terra_address_to_hex(human_addr: string) {
|
||||
return "0x" + toHex(zeroPad(Bech32.decode(human_addr).data, 32));
|
||||
return "0x" + toHex(ethers.utils.zeroPad(Bech32.decode(human_addr).data, 32));
|
||||
}
|
||||
|
||||
// enter key of what to extract
|
||||
|
|
Loading…
Reference in New Issue