Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a6fe785

Browse files
authoredJul 23, 2023
Cosmos docs (#38)
* initial cosmwasm * add network selector * cosmos * working on rust validation * add rust code tests * add more networks * fix some stuff * fix stuff * dont need this * ok * hrm * kind of works * improve stuff * ok pretty close now * pretty good shape * cool * stuff * cleanup * lint * cleanup * grr * gah * graz * graz * ugh * address comment
1 parent 64decc4 commit a6fe785

27 files changed

+3352
-150
lines changed
 

‎__tests__/validateCodeSnippets.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "fs";
33
import { tmpdir } from "os";
44
import { join } from "path";
55
import { promisify } from "util";
6+
import toml from "toml";
67

78
const codeSnippetRegex = /```([a-zA-Z]+)[\s\S]*?```/g;
89
const codeSnippetsDir = ".code_tests";
@@ -73,22 +74,66 @@ async function runCodeSnippet(
7374
language: string,
7475
id: string
7576
): Promise<[boolean, string]> {
76-
let command: string;
77-
7877
if (language === "typescript") {
7978
const tempFilePath = join(codeSnippetsDir, `${id}.ts`);
8079
fs.writeFileSync(tempFilePath, wrapTsCodeInAsyncFunction(code), "utf8");
8180
// Note: it's unfortunate that we have to repeat the tsconfig options here, but there doesn't
8281
// seem to be a way to read the flags in the config file from the command line without getting the entire project.
83-
command = `npx tsc --target es5 --module esnext --esModuleInterop --moduleResolution node --skipLibCheck --resolveJsonModule --noEmit ${tempFilePath}`;
82+
const command = `npx tsc --target es5 --module esnext --esModuleInterop --moduleResolution node --skipLibCheck --resolveJsonModule --noEmit ${tempFilePath}`;
83+
return await runValidationCommand(command);
8484
} else if (language === "solidity") {
8585
const tempFilePath = join(codeSnippetsDir, `${id}.sol`);
8686
fs.writeFileSync(tempFilePath, wrapSolCode(code), "utf8");
87-
command = `npx solc --base-path . --include-path node_modules/\\@pythnetwork --output-dir ${tmpdir()} --bin ${tempFilePath}`;
87+
const command = `npx solc --base-path . --include-path node_modules/\\@pythnetwork --output-dir ${tmpdir()} --bin ${tempFilePath}`;
88+
return await runValidationCommand(command);
89+
} else if (language === "rust") {
90+
// Rust files get a separate directory with a full cargo project.
91+
const tempFilePath = join(codeSnippetsDir, `${id}`);
92+
fs.mkdirSync(tempFilePath);
93+
fs.writeFileSync(join(tempFilePath, "lib.rs"), code, "utf8");
94+
fs.writeFileSync(
95+
join(tempFilePath, "Cargo.toml"),
96+
generateCargoText(),
97+
"utf8"
98+
);
99+
// cargo check checks syntax without doing a full build. It's faster (but still pretty slow)
100+
const command = `cd ${tempFilePath} && cargo check`;
101+
return await runValidationCommand(command);
102+
} else if (language === "json") {
103+
const tempFilePath = join(codeSnippetsDir, `${id}.json`);
104+
fs.writeFileSync(tempFilePath, code, "utf8");
105+
return runValidationFunction(code, (x) => JSON.parse(x));
106+
} else if (language === "toml") {
107+
const tempFilePath = join(codeSnippetsDir, `${id}.json`);
108+
fs.writeFileSync(tempFilePath, code, "utf8");
109+
return runValidationFunction(code, (x) => toml.parse(x));
88110
} else {
89111
return [false, `Unsupported language: ${language}`];
90112
}
113+
}
91114

115+
// Cargo file for rust validation. Expects the source code to be in lib.rs
116+
function generateCargoText() {
117+
return `
118+
[package]
119+
name = "pyth-documentation-snippets"
120+
version = "0.0.1"
121+
authors = ["Pyth Network Contributors"]
122+
edition = "2018"
123+
124+
[dependencies]
125+
pyth-sdk-cw = "1.0.0"
126+
cosmwasm-std = "1.0.0"
127+
128+
[lib]
129+
name = "unused"
130+
path = "lib.rs"
131+
`;
132+
}
133+
134+
async function runValidationCommand(
135+
command: string
136+
): Promise<[boolean, string]> {
92137
try {
93138
const result = await execPromise(command);
94139
return [true, JSON.stringify(result.stdout)];
@@ -98,6 +143,15 @@ async function runCodeSnippet(
98143
}
99144
}
100145

146+
async function runValidationFunction(input: string, f: (string) => void) {
147+
try {
148+
f(input);
149+
return [true, input];
150+
} catch (error) {
151+
return [false, error.toString()];
152+
}
153+
}
154+
101155
// Delete everything in the code snippets directory
102156
function clearCodeSnippetsDir(): void {
103157
if (fs.existsSync(codeSnippetsDir)) {
@@ -146,4 +200,5 @@ describe("Validate code snippets", () => {
146200
// We only validate code snippets in the API reference.
147201
// However, we exclude Aptos for now because it's annoying (and doesn't seem worth it).
148202
validateCodeSnippets("./pages/evm");
203+
validateCodeSnippets("./pages/cosmwasm");
149204
});

‎components/CosmWasmExecute.tsx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import {
3+
useAccount,
4+
useDisconnect,
5+
useExecuteContract,
6+
useSuggestChainAndConnect,
7+
} from "graz";
8+
9+
import {
10+
getCosmosChainFromConfig,
11+
useGlobalContext,
12+
} from "../contexts/GlobalContext";
13+
import { JsonObject } from "@cosmjs/cosmwasm-stargate";
14+
import CosmosNetworkSelector from "./CosmosNetworkSelector";
15+
import { Coin, coin } from "@cosmjs/proto-signing";
16+
17+
interface CosmWasmExecuteProps {
18+
buildMsg: (kvs: Record<string, string>) => JsonObject | undefined;
19+
feeKey: string | undefined;
20+
}
21+
22+
/**
23+
* Allow the user to send an execute message to a cosmos chain and visualize the response.
24+
* This component will invoke `buildMsg` on the global context to create the message.
25+
* `buildMsg` may return `undefined` to indicate that the key-value store does not contain all
26+
* of the necessary arguments.
27+
*
28+
* The component will also send a quantity of native tokens with the message. The quantity is
29+
* determined by looking up the value of `feeKey`.
30+
*/
31+
const CosmWasmExecute = ({ buildMsg, feeKey }: CosmWasmExecuteProps) => {
32+
const { keyValueStore, cosmosChainConfig } = useGlobalContext();
33+
34+
const [response, setResponse] = useState<string>();
35+
const [responsePreface, setResponsePreface] = useState<string>();
36+
37+
const { isConnected } = useAccount();
38+
const { error, isLoading, isSuccess, executeContractAsync } =
39+
useExecuteContract<JsonObject>({
40+
contractAddress: cosmosChainConfig.pythAddress,
41+
});
42+
43+
useEffect(() => {
44+
clearResponse();
45+
}, [keyValueStore, cosmosChainConfig]);
46+
47+
const clearResponse = async () => {
48+
setResponsePreface(undefined);
49+
setResponse(undefined);
50+
};
51+
52+
useEffect(() => {
53+
if (isLoading) {
54+
setResponsePreface("Loading...");
55+
setResponse("");
56+
} else if (isSuccess) {
57+
// Ignore because we populate this field immediately after executing.
58+
// (useExecuteContract doesn't return the result for whatever reason)
59+
} else if (error) {
60+
setResponsePreface("Contract execution reverted with exception:");
61+
setResponse(error.toString());
62+
}
63+
}, [isLoading, isSuccess, error]);
64+
65+
const executeQuery = async () => {
66+
const msgJson: JsonObject | undefined = buildMsg(keyValueStore);
67+
68+
let funds: Coin[] | undefined = [];
69+
if (feeKey !== undefined) {
70+
// Note that we assume that fees are paid in the 1st fee currency.
71+
// This should work, though doesn't demonstrate the full range of functionality for chains
72+
// like osmosis that support multiple fee currencies.
73+
const cosmosChain = getCosmosChainFromConfig(cosmosChainConfig.chainId);
74+
const fee = keyValueStore[feeKey];
75+
if (fee !== undefined) {
76+
funds = [coin(fee, cosmosChain.feeCurrencies[0].coinMinimalDenom)];
77+
} else {
78+
funds = undefined;
79+
}
80+
}
81+
82+
if (msgJson === undefined || funds === undefined) {
83+
setResponsePreface(
84+
`Please populate all of the arguments with valid values.`
85+
);
86+
setResponse(undefined);
87+
} else {
88+
try {
89+
const result = await executeContractAsync({
90+
msg: msgJson,
91+
funds,
92+
});
93+
94+
setResponsePreface("Contract execution succeeded with result:");
95+
setResponse(JSON.stringify(result, null, 2));
96+
} catch (error) {
97+
// This catch prevents nextra from reporting the error in a modal. No need to update
98+
// the component state though, because the error also gets returned by useExecuteContract.
99+
}
100+
}
101+
};
102+
103+
return (
104+
<>
105+
<div className="my-4 flex space-x-2 mb-4">
106+
<CosmWasmAccountButton /> <CosmosNetworkSelector />
107+
</div>
108+
<div className="flex">
109+
{isConnected && (
110+
<div className="space-x-2 mb-4">
111+
<button
112+
className="bg-[#E6DAFE] text-[#141227] font-normal text-base hover:bg-[#F2ECFF]"
113+
onClick={executeQuery}
114+
>
115+
execute query
116+
</button>
117+
<button className="font-normal text-base" onClick={clearResponse}>
118+
clear
119+
</button>
120+
</div>
121+
)}
122+
</div>
123+
<div>
124+
{responsePreface !== undefined && (
125+
<div className="response">
126+
<p className="px-4">{responsePreface}</p>
127+
<pre>{response}</pre>
128+
</div>
129+
)}
130+
</div>
131+
</>
132+
);
133+
};
134+
135+
export const CosmWasmAccountButton = () => {
136+
const { isConnected, data: account } = useAccount();
137+
const { suggestAndConnect } = useSuggestChainAndConnect();
138+
const { disconnect } = useDisconnect();
139+
140+
const { cosmosChainConfig } = useGlobalContext();
141+
142+
const handleSuggestAndConnect = useCallback(() => {
143+
const cosmosChain = getCosmosChainFromConfig(cosmosChainConfig.chainId);
144+
suggestAndConnect({
145+
chainInfo: cosmosChain,
146+
// TODO: not clear that setting this to 0.1 is going to be enough to land on-chain,
147+
// but there doesn't seem to be a good way to dynamically set this price.
148+
gas: {
149+
price: "0.1",
150+
denom: cosmosChain.feeCurrencies[0].coinMinimalDenom,
151+
},
152+
});
153+
}, [cosmosChainConfig, suggestAndConnect]);
154+
155+
useEffect(() => {
156+
handleSuggestAndConnect();
157+
}, [cosmosChainConfig, handleSuggestAndConnect]);
158+
159+
if (isConnected) {
160+
return (
161+
<button className="font-normal text-base" onClick={() => disconnect()}>
162+
{`${account?.bech32Address}`}
163+
</button>
164+
);
165+
} else {
166+
return (
167+
<button
168+
className="bg-[#E6DAFE] text-[#141227] font-normal text-base hover:bg-[#F2ECFF]"
169+
onClick={() => handleSuggestAndConnect()}
170+
>
171+
Connect Wallet
172+
</button>
173+
);
174+
}
175+
};
176+
177+
export default CosmWasmExecute;

‎components/CosmWasmQuery.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useEffect, useState } from "react";
2+
import {
3+
getCosmosChainFromConfig,
4+
useGlobalContext,
5+
} from "../contexts/GlobalContext";
6+
import CosmosNetworkSelector from "./CosmosNetworkSelector";
7+
import { CosmWasmClient, JsonObject } from "@cosmjs/cosmwasm-stargate";
8+
9+
interface CosmWasmQueryProps {
10+
buildQuery: (kvs: Record<string, string>) => JsonObject | undefined;
11+
}
12+
13+
/**
14+
* Allow the user to send a query message to a cosmos chain and visualize the response.
15+
* This component will invoke `buildMsg` on the global context to create the message.
16+
* `buildMsg` may return `undefined` to indicate that the key-value store does not contain all
17+
* of the necessary arguments.
18+
*/
19+
const CosmWasmQuery = ({ buildQuery }: CosmWasmQueryProps) => {
20+
const [response, setResponse] = useState<string>();
21+
// The preface is explainer text that shows up before the response itself.
22+
const [responsePreface, setResponsePreface] = useState<string>();
23+
24+
const { keyValueStore, cosmosChainConfig } = useGlobalContext();
25+
26+
useEffect(() => {
27+
clearResponse();
28+
}, [keyValueStore, cosmosChainConfig]);
29+
30+
const sendTransaction = async () => {
31+
const queryJson: JsonObject | undefined = buildQuery(keyValueStore);
32+
33+
// TODO: validate arguments
34+
if (queryJson === undefined) {
35+
setResponsePreface(
36+
`Please populate all of the arguments with valid values.`
37+
);
38+
} else {
39+
try {
40+
setResponsePreface("Loading...");
41+
setResponse(undefined);
42+
43+
const chain = getCosmosChainFromConfig(cosmosChainConfig.chainId);
44+
const client = await CosmWasmClient.connect(chain.rpc);
45+
46+
const result = await client.queryContractSmart(
47+
cosmosChainConfig.pythAddress,
48+
queryJson
49+
);
50+
51+
const resultString = JSON.stringify(result, null, 2);
52+
53+
setResponsePreface("Query succeeded with result:");
54+
setResponse(resultString);
55+
} catch (error) {
56+
setResponsePreface("Query failed with error:");
57+
setResponse((error as any).toString());
58+
}
59+
}
60+
};
61+
62+
const clearResponse = async () => {
63+
setResponsePreface(undefined);
64+
setResponse(undefined);
65+
};
66+
67+
return (
68+
<div>
69+
<div className="flex flex-col md:flex-row md:justify-between">
70+
<div className="space-x-2 mt-4 md:my-4">
71+
<button
72+
className="bg-[#E6DAFE] text-[#141227] font-normal text-base hover:bg-[#F2ECFF]"
73+
onClick={sendTransaction}
74+
>
75+
execute query
76+
</button>
77+
<button className="font-normal text-base" onClick={clearResponse}>
78+
clear
79+
</button>
80+
</div>
81+
<div className="my-4">
82+
<CosmosNetworkSelector />
83+
</div>
84+
</div>
85+
{responsePreface !== undefined && (
86+
<div className="response">
87+
<p className="px-4">{responsePreface}</p>
88+
<pre>{response}</pre>
89+
</div>
90+
)}
91+
</div>
92+
);
93+
};
94+
95+
export default CosmWasmQuery;

‎components/CosmosNetworkSelector.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
PythCosmosAddresses,
3+
useGlobalContext,
4+
} from "../contexts/GlobalContext";
5+
import Selector from "./Selector";
6+
7+
/** Drop-down selection component for choosing the current cosmos network. */
8+
const CosmosNetworkSelector = () => {
9+
const { cosmosChainId, setCosmosChainId } = useGlobalContext();
10+
// Get the network names as an array
11+
const networkNames = Object.keys(PythCosmosAddresses);
12+
13+
return (
14+
<Selector
15+
values={networkNames}
16+
currentValue={cosmosChainId}
17+
onChange={setCosmosChainId}
18+
/>
19+
);
20+
};
21+
22+
export default CosmosNetworkSelector;

1 commit comments

Comments
 (1)

vercel[bot] commented on Jul 23, 2023

@vercel[bot]
Please sign in to comment.