wormchain - contracts deploy
This commit is contained in:
parent
a9373d54fa
commit
ab6146f19b
|
@ -43,6 +43,27 @@ ts-sdk: vue
|
|||
run: build/wormchaind
|
||||
./$< start --home build --log_level="debug"
|
||||
|
||||
# get npm packages for contracts/tools
|
||||
contracts-tools-deps: contracts/tools/package-lock.json
|
||||
npm ci --prefix=contracts/tools
|
||||
|
||||
# get .env and devnet-consts.json for contracts/tools
|
||||
contracts-devnet-env:
|
||||
cd .. && ./scripts/guardian-set-init.sh 1
|
||||
cd .. && ./scripts/distribute-devnet-consts.sh
|
||||
|
||||
# get wasm artifacts for cosmwasm contracts
|
||||
contracts-artifacts:
|
||||
cd ../cosmwasm && $(MAKE) artifacts
|
||||
cp -r ../cosmwasm/artifacts contracts
|
||||
|
||||
# get everything needed to
|
||||
contracts-deploy-setup: contracts-tools-deps contracts-devnet-env contracts-artifacts
|
||||
|
||||
# runs the contract deployment script
|
||||
contracts-deploy-local: contracts-deploy-setup
|
||||
npm run deploy-wormchain --prefix=contracts/tools
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v ./...
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
import "dotenv/config";
|
||||
import * as os from "os"
|
||||
import { SigningCosmWasmClient, InstantiateResult } from "@cosmjs/cosmwasm-stargate";
|
||||
import { GasPrice } from "@cosmjs/stargate"
|
||||
import { Secp256k1HdWallet } from "@cosmjs/amino";
|
||||
import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx";
|
||||
import * as fs from "fs";
|
||||
import { readdirSync, } from "fs";
|
||||
import * as util from 'util'
|
||||
import { toUtf8 } from "@cosmjs/encoding";
|
||||
|
||||
import * as devnetConsts from "./devnet-consts.json"
|
||||
|
||||
if (process.env.INIT_SIGNERS === "undefined") {
|
||||
let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`
|
||||
console.error(msg)
|
||||
throw msg
|
||||
}
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
/*
|
||||
NOTE: Only append to this array: keeping the ordering is crucial, as the
|
||||
contracts must be imported in a deterministic order so their addresses remain
|
||||
deterministic.
|
||||
*/
|
||||
type ContractName = string
|
||||
const artifacts: ContractName[] = [
|
||||
"cw20_base.wasm",
|
||||
"wormchain_accounting.wasm",
|
||||
];
|
||||
|
||||
const ARTIFACTS_PATH = "../artifacts/"
|
||||
/* Check that the artifact folder contains all the wasm files we expect and nothing else */
|
||||
|
||||
try {
|
||||
const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) =>
|
||||
a.endsWith(".wasm")
|
||||
);
|
||||
|
||||
const missing_artifacts = artifacts.filter(
|
||||
(a) => !actual_artifacts.includes(a)
|
||||
);
|
||||
if (missing_artifacts.length) {
|
||||
console.log(
|
||||
"Error during wormchain deployment. The following files are expected to be in the artifacts folder:"
|
||||
);
|
||||
missing_artifacts.forEach((file) => console.log(` - ${file}`));
|
||||
console.log(
|
||||
"Hint: the deploy script needs to run after the contracts have been built."
|
||||
);
|
||||
console.log(
|
||||
"External binary blobs need to be manually added in tools/Dockerfile."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`${ARTIFACTS_PATH} cannot be read. Do you need to run "make contracts-deploy-setup"?`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function main() {
|
||||
|
||||
/* Set up cosmos client & wallet */
|
||||
|
||||
let host = devnetConsts.chains[3104].tendermintUrlLocal
|
||||
if (os.hostname().includes("wormchain-deploy")) {
|
||||
// running in tilt devnet
|
||||
host = devnetConsts.chains[3104].tendermintUrlTilt
|
||||
}
|
||||
const denom = devnetConsts.chains[3104].addresses.native.denom
|
||||
const mnemonic = devnetConsts.chains[3104].accounts.wormchainNodeOfGuardian0.mnemonic
|
||||
const addressPrefix = "wormhole"
|
||||
|
||||
const w = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: addressPrefix })
|
||||
|
||||
|
||||
const gas = GasPrice.fromString(`0${denom}`)
|
||||
let cwc: SigningCosmWasmClient
|
||||
try {
|
||||
cwc = await SigningCosmWasmClient.connectWithSigner(host, w, { prefix: addressPrefix, gasPrice: gas })
|
||||
} catch (e) {
|
||||
let msg = `could not connect to wormchain host: ${host}`
|
||||
if (e?.message) {
|
||||
console.error(e.message)
|
||||
}
|
||||
throw msg
|
||||
}
|
||||
|
||||
|
||||
// there are several Cosmos chains in devnet, so check the config is as expected
|
||||
let id = await cwc.getChainId()
|
||||
if (id !== "wormchain") {
|
||||
throw new Error(`Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}`)
|
||||
}
|
||||
|
||||
const signers = await w.getAccounts()
|
||||
const signer = signers[0].address
|
||||
console.log("wormchain contract deployer is: ", signer)
|
||||
|
||||
/* Deploy artifacts */
|
||||
|
||||
const codeIds: { [name: ContractName]: number } = await artifacts.reduce(async (prev, file) => {
|
||||
// wait for the previous to finish, to avoid the race condition of wallet sequence mismatch.
|
||||
const accum = await prev
|
||||
|
||||
const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`);
|
||||
|
||||
const i = await cwc.upload(signer, contract_bytes, "auto", "")
|
||||
console.log(`uploaded ${file}, codeID: ${i.codeId}, tx: ${i.transactionHash}`, i.codeId, i.transactionHash)
|
||||
|
||||
accum[file] = i.codeId
|
||||
return accum
|
||||
}, Object())
|
||||
|
||||
// Instantiate contracts.
|
||||
|
||||
async function instantiate(code_id: number, inst_msg: any, label: string) {
|
||||
let inst = await cwc.instantiate(signer, code_id, inst_msg, label, "auto", {})
|
||||
let addr = inst.contractAddress
|
||||
let txHash = inst.transactionHash
|
||||
console.log(`deployed contract ${label}, codeID: ${code_id}, address: ${addr}, txHash: ${txHash}`)
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// Instantiate contracts.
|
||||
// NOTE: Only append at the end, the ordering must be deterministic.
|
||||
|
||||
const addresses: { [contractName: string]: InstantiateResult["transactionHash"] } = {};
|
||||
|
||||
const init_guardians: string[] = JSON.parse(String(process.env.INIT_SIGNERS));
|
||||
if (!init_guardians || init_guardians.length === 0) {
|
||||
throw "failed to get initial guardians from .env file.";
|
||||
}
|
||||
|
||||
addresses["mock.wasm"] = await instantiate(
|
||||
codeIds["cw20_base.wasm"],
|
||||
{
|
||||
name: "MOCK",
|
||||
symbol: "MCK",
|
||||
decimals: 6,
|
||||
initial_balances: [
|
||||
{
|
||||
address: signer,
|
||||
amount: "100000000",
|
||||
},
|
||||
],
|
||||
mint: null,
|
||||
},
|
||||
"mock"
|
||||
);
|
||||
console.log('instantiated cw20 MOCK token: ', addresses["mock.wasm"])
|
||||
|
||||
|
||||
const registrations: { [chainName: string]: string } = {
|
||||
// keys are only used for logging success/failure
|
||||
"solana": String(process.env.REGISTER_SOL_TOKEN_BRIDGE_VAA),
|
||||
"ethereum": String(process.env.REGISTER_ETH_TOKEN_BRIDGE_VAA),
|
||||
"bsc": String(process.env.REGISTER_BSC_TOKEN_BRIDGE_VAA),
|
||||
"algo": String(process.env.REGISTER_ALGO_TOKEN_BRIDGE_VAA),
|
||||
"terra": String(process.env.REGISTER_TERRA_TOKEN_BRIDGE_VAA),
|
||||
"near": String(process.env.REGISTER_NEAR_TOKEN_BRIDGE_VAA),
|
||||
"terra2": String(process.env.REGISTER_TERRA2_TOKEN_BRIDGE_VAA),
|
||||
"aptos": String(process.env.REGISTER_APTOS_TOKEN_BRIDGE_VAA),
|
||||
}
|
||||
|
||||
|
||||
const instantiateMsg = {}
|
||||
addresses["wormchain_accounting.wasm"] = await instantiate(
|
||||
codeIds["wormchain_accounting.wasm"],
|
||||
instantiateMsg,
|
||||
"wormchainAccounting"
|
||||
)
|
||||
console.log("instantiated accounting: ", addresses["wormchain_accounting.wasm"])
|
||||
|
||||
const accountingRegistrations = Object.values(registrations)
|
||||
.map(r => Buffer.from(r, "hex").toString("base64"))
|
||||
|
||||
const msg = {
|
||||
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
|
||||
value: MsgExecuteContract.fromPartial({
|
||||
sender: signer,
|
||||
contract: addresses["wormchain_accounting.wasm"],
|
||||
msg: toUtf8(JSON.stringify({
|
||||
submit_v_a_as: {
|
||||
vaas: accountingRegistrations,
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
const res = await cwc.signAndBroadcast(signer, [msg], "auto");
|
||||
console.log(`sent accounting chain registrations, tx: `, res.transactionHash);
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
main()
|
||||
} catch (e: any) {
|
||||
if (e?.message) {
|
||||
console.error(e.message)
|
||||
}
|
||||
throw e
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
import "dotenv/config";
|
||||
import * as os from "os"
|
||||
import { SigningCosmWasmClient, toBinary } from "@cosmjs/cosmwasm-stargate";
|
||||
import { GasPrice } from "@cosmjs/stargate"
|
||||
import { fromBase64 } from "cosmwasm";
|
||||
import { Secp256k1HdWallet } from "@cosmjs/amino";
|
||||
|
||||
import { zeroPad } from "ethers/lib/utils.js";
|
||||
import { keccak256 } from "@cosmjs/crypto"
|
||||
|
||||
import * as elliptic from "elliptic"
|
||||
import { concatArrays, encodeUint8 } from "./utils";
|
||||
|
||||
import * as devnetConsts from "./devnet-consts.json"
|
||||
|
||||
|
||||
function signBinary(key: elliptic.ec.KeyPair, binary: string): Uint8Array {
|
||||
// base64 string to Uint8Array,
|
||||
// so we have bytes to work with for signing, though not sure 100% that's correct.
|
||||
const bytes = fromBase64(binary);
|
||||
|
||||
// create the "digest" for signing.
|
||||
// The contract will calculate the digest of the "data",
|
||||
// then use that with the signature to ec recover the publickey that signed.
|
||||
const digest = keccak256(keccak256(bytes));
|
||||
|
||||
// sign the digest
|
||||
const signature = key.sign(digest, { canonical: true });
|
||||
|
||||
// create 65 byte signature (64 + 1)
|
||||
const signedParts = [
|
||||
zeroPad(signature.r.toBuffer(), 32),
|
||||
zeroPad(signature.s.toBuffer(), 32),
|
||||
encodeUint8(signature.recoveryParam || 0),
|
||||
];
|
||||
|
||||
// combine parts to be Uint8Array with length 65
|
||||
const signed = concatArrays(signedParts);
|
||||
|
||||
return signed
|
||||
}
|
||||
|
||||
|
||||
async function main() {
|
||||
|
||||
/* Set up cosmos client & wallet */
|
||||
|
||||
const WORMCHAIN_ID = 3104
|
||||
|
||||
let host = devnetConsts.chains[3104].tendermintUrlLocal
|
||||
if (os.hostname().includes("wormchain-deploy")) {
|
||||
// running in tilt devnet
|
||||
host = devnetConsts.chains[3104].tendermintUrlTilt
|
||||
}
|
||||
const denom = devnetConsts.chains[WORMCHAIN_ID].addresses.native.denom
|
||||
const mnemonic = devnetConsts.chains[WORMCHAIN_ID].accounts.wormchainNodeOfGuardian0.mnemonic
|
||||
const addressPrefix = "wormhole"
|
||||
const signerPk = devnetConsts.devnetGuardians[0].private
|
||||
const accountingAddress = devnetConsts.chains[WORMCHAIN_ID].contracts.accountingNativeAddress
|
||||
|
||||
const w = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: addressPrefix })
|
||||
|
||||
const gas = GasPrice.fromString(`0${denom}`)
|
||||
let cwc = await SigningCosmWasmClient.connectWithSigner(host, w, { prefix: addressPrefix, gasPrice: gas })
|
||||
|
||||
// there is no danger here, just several Cosmos chains in devnet, so check for config issues
|
||||
let id = await cwc.getChainId()
|
||||
if (id !== "wormchain") {
|
||||
throw new Error(`Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}`)
|
||||
}
|
||||
|
||||
const signers = await w.getAccounts()
|
||||
const signer = signers[0].address
|
||||
console.log("wormchain wallet pubkey: ", signer)
|
||||
|
||||
const nativeBalance = await cwc.getBalance(signer, denom)
|
||||
console.log("nativeBalance ", nativeBalance.amount)
|
||||
|
||||
const utestBalance = await cwc.getBalance(signer, "utest")
|
||||
console.log("utest balance ", utestBalance.amount)
|
||||
|
||||
|
||||
// create key for guardian0
|
||||
const ec = new elliptic.ec("secp256k1");
|
||||
// create key from the devnet guardian0's private key
|
||||
const key = ec.keyFromPrivate(Buffer.from(signerPk, "hex"));
|
||||
|
||||
|
||||
// Test empty observation
|
||||
|
||||
// object to json string, then to base64 (serde binary)
|
||||
const arrayBinaryString = toBinary([]);
|
||||
|
||||
// combine parts to be Uint8Array with length 65
|
||||
const signedEmptyArray = signBinary(key, arrayBinaryString)
|
||||
|
||||
const observeEmptyArray = {
|
||||
submit_observations: {
|
||||
observations: arrayBinaryString,
|
||||
guardian_set_index: 0,
|
||||
signature: {
|
||||
index: 0,
|
||||
signature: Array.from(signedEmptyArray),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let emptyArrayObsRes = await cwc.execute(signer, accountingAddress, observeEmptyArray, "auto");
|
||||
console.log(`emptyArrayObsRes.transactionHash: ${emptyArrayObsRes.transactionHash}`);
|
||||
|
||||
|
||||
// Test (fake) observation
|
||||
const emitter_address = "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16"
|
||||
const observations = [
|
||||
{
|
||||
emitter_chain: 2,
|
||||
emitter_address: emitter_address,
|
||||
sequence: 2,
|
||||
nonce: 1,
|
||||
consistency_level: 0,
|
||||
timestamp: 1,
|
||||
payload:
|
||||
Buffer.from("030000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000002d8be6bf0baa74e0a907016679cae9190e80dd0a0002000000000000000000000000c10820983f33456ce7beb3a046f5a83fa34f027d0c2000000000000000000000000000000000000000000000000000000000000f4240", "hex").toString("base64"),
|
||||
|
||||
tx_hash:
|
||||
Buffer.from("9fc68fb0ee735d45c9074a20adef1747b0593803f33b9f3f2252c8e2df567f41", "hex").toString("base64")
|
||||
},
|
||||
];
|
||||
|
||||
// object to json string, then to base64 (serde binary)
|
||||
const observationsBinaryString = toBinary(observations);
|
||||
|
||||
const signed = signBinary(key, observationsBinaryString)
|
||||
|
||||
const executeMsg = {
|
||||
submit_observations: {
|
||||
observations: observationsBinaryString,
|
||||
guardian_set_index: 0,
|
||||
signature: {
|
||||
index: 0,
|
||||
signature: Array.from(signed),
|
||||
},
|
||||
},
|
||||
};
|
||||
console.log(executeMsg);
|
||||
|
||||
let inst = await cwc.execute(
|
||||
signer,
|
||||
accountingAddress,
|
||||
executeMsg,
|
||||
"auto"
|
||||
);
|
||||
let txHash = inst.transactionHash;
|
||||
console.log(`executed submit_observation! txHash: ${txHash}`);
|
||||
|
||||
|
||||
|
||||
console.log("done, exiting success.")
|
||||
}
|
||||
|
||||
try {
|
||||
main()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
export function concatArrays(arrays: Uint8Array[]): Uint8Array {
|
||||
const totalLength = arrays.reduce((accum, x) => accum + x.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
|
||||
for (let i = 0, offset = 0; i < arrays.length; i++) {
|
||||
result.set(arrays[i], offset);
|
||||
offset += arrays[i].length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export function encodeUint8(value: number): Uint8Array {
|
||||
if (value >= 2 ** 8 || value < 0) {
|
||||
throw new Error(`Out of bound value in Uint8: ${value}`);
|
||||
}
|
||||
|
||||
return new Uint8Array([value]);
|
||||
}
|
Loading…
Reference in New Issue