Skip to content

Commit e667439

Browse files
authored
Merge pull request #103 from polkadot-api/vo/view-fns
feat: add view functions
2 parents d4c8945 + 1daed01 commit e667439

File tree

8 files changed

+528
-13
lines changed

8 files changed

+528
-13
lines changed

src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { VaultTxModal } from "polkahub"
12
import { Navigate, Route, Routes } from "react-router-dom"
23
import { Accounts } from "./pages/Accounts/Accounts"
34
import { Constants } from "./pages/Constants"
@@ -9,7 +10,7 @@ import { RpcCalls } from "./pages/RpcCalls"
910
import { RuntimeCalls } from "./pages/RuntimeCalls"
1011
import { Storage } from "./pages/Storage"
1112
import { Transactions } from "./pages/Transactions"
12-
import { VaultTxModal } from "polkahub"
13+
import { ViewFns } from "./pages/ViewFns"
1314

1415
export default function App() {
1516
return (
@@ -26,6 +27,7 @@ export default function App() {
2627
<Route path="rpcCalls/*" element={<RpcCalls />} />
2728
<Route path="metadata/*" element={<Metadata />} />
2829
<Route path="accounts/*" element={<Accounts />} />
30+
<Route path="viewFns/*" element={<ViewFns />} />
2931
<Route path="*" element={<Navigate to="/explorer" replace />} />
3032
</Routes>
3133
</div>

src/pages/Header.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export const Header = () => {
8282
</NavLink>
8383
))}
8484
</nav>
85+
<NavLink to="/viewFns" onClick={() => setIsOpen(false)}>
86+
View Functions
87+
</NavLink>
8588
<NavLink to="/metadata" onClick={() => setIsOpen(false)}>
8689
Metadata
8790
</NavLink>

src/pages/Metadata/Pallets.tsx

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { CopyText } from "@/components/Copy"
12
import { SearchableSelect } from "@/components/Select"
23
import { UnifiedMetadata } from "@polkadot-api/substrate-bindings"
3-
import { FC, useEffect, useState } from "react"
4+
import { FC, Fragment, useState } from "react"
45
import { LookupLink } from "./Lookup"
5-
import { CopyText } from "@/components/Copy"
66

77
type Pallet = UnifiedMetadata["pallets"][number]
88
export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
@@ -22,7 +22,7 @@ export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
2222
/>
2323
</label>
2424
{pallet && (
25-
<>
25+
<Fragment key={pallet.name}>
2626
<p>Index: {pallet.index}</p>
2727
{pallet.storage && (
2828
<div>
@@ -36,6 +36,12 @@ export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
3636
<PalletConstants pallet={pallet} />
3737
</div>
3838
)}
39+
{pallet.viewFns.length > 0 && (
40+
<div>
41+
<h4>View Functions</h4>
42+
<PalletViewFns pallet={pallet} />
43+
</div>
44+
)}
3945
{pallet.calls != null && (
4046
<div>
4147
<h4>Calls</h4>
@@ -54,7 +60,7 @@ export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
5460
<LookupLink id={pallet.errors.type} />
5561
</div>
5662
)}
57-
</>
63+
</Fragment>
5864
)}
5965
</div>
6066
)
@@ -68,10 +74,6 @@ type StorageEntry = Pallet["storage"] extends
6874
const PalletStorage: FC<{ pallet: Pallet }> = ({ pallet }) => {
6975
const [entry, setEntry] = useState<StorageEntry | null>(null)
7076

71-
useEffect(() => {
72-
setEntry(null)
73-
}, [pallet])
74-
7577
if (!pallet.storage) return null
7678

7779
const value =
@@ -129,10 +131,6 @@ type ConstantEntry = Pallet["constants"] extends Array<infer R> ? R : never
129131
const PalletConstants: FC<{ pallet: Pallet }> = ({ pallet }) => {
130132
const [entry, setEntry] = useState<ConstantEntry | null>(null)
131133

132-
useEffect(() => {
133-
setEntry(null)
134-
}, [pallet])
135-
136134
if (!pallet.constants.length) return null
137135

138136
return (
@@ -166,3 +164,42 @@ const PalletConstants: FC<{ pallet: Pallet }> = ({ pallet }) => {
166164
</div>
167165
)
168166
}
167+
168+
type ViewFnEntry = Pallet["viewFns"] extends Array<infer R> ? R : never
169+
const PalletViewFns: FC<{ pallet: Pallet }> = ({ pallet }) => {
170+
const [entry, setEntry] = useState<ViewFnEntry | null>(null)
171+
172+
if (!pallet.viewFns.length) return null
173+
174+
return (
175+
<div className="flex flex-col p-2 gap-2">
176+
<SearchableSelect
177+
value={entry}
178+
setValue={setEntry}
179+
options={pallet.viewFns.map((c) => ({
180+
text: c.name,
181+
value: c,
182+
}))}
183+
/>
184+
{entry && (
185+
<>
186+
<div>
187+
<h4>Inputs</h4>
188+
<ol>
189+
{entry.inputs.map((input) => (
190+
<li key={input.name}>
191+
<div>{input.name}</div>
192+
<LookupLink id={input.type} />
193+
</li>
194+
))}
195+
</ol>
196+
</div>
197+
<div>
198+
<h4>Output</h4>
199+
<LookupLink id={entry.output} />
200+
</div>
201+
</>
202+
)}
203+
</div>
204+
)
205+
}

src/pages/ViewFns/ViewFnQuery.tsx

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {
2+
dynamicBuilder$,
3+
lookup$,
4+
unsafeApi$,
5+
} from "@/state/chains/chain.state"
6+
import {
7+
InlineLookupTypeEdit,
8+
LookupTypeEdit,
9+
} from "@/codec-components/LookupTypeEdit"
10+
import { ActionButton } from "@/components/ActionButton"
11+
import { ExpandBtn } from "@/components/Expand"
12+
import { getTypeComplexity } from "@/utils/shape"
13+
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
14+
import { createSignal } from "@react-rxjs/utils"
15+
import { Circle, Dot } from "lucide-react"
16+
import { FC, useState } from "react"
17+
import {
18+
combineLatest,
19+
filter,
20+
firstValueFrom,
21+
map,
22+
scan,
23+
startWith,
24+
switchMap,
25+
} from "rxjs"
26+
import { twMerge } from "tailwind-merge"
27+
import { addViewFnCall, selectedEntry$ } from "./viewFns.state"
28+
29+
export const ViewFnQuery: FC = () => {
30+
const selectedEntry = useStateObservable(selectedEntry$)
31+
const isReady = useStateObservable(isReady$)
32+
33+
if (!selectedEntry) return null
34+
35+
const submit = async () => {
36+
const [entry, unsafeApi, inputValues, builder] = await firstValueFrom(
37+
combineLatest([
38+
selectedEntry$,
39+
unsafeApi$,
40+
inputValues$,
41+
dynamicBuilder$,
42+
]),
43+
)
44+
const decodedValues = inputValues.map((v, i) =>
45+
builder
46+
.buildDefinition(selectedEntry.inputs[i].type)
47+
.dec(v as Uint8Array),
48+
)
49+
const promise = unsafeApi.view[entry!.pallet][entry!.name](...decodedValues)
50+
51+
addViewFnCall({
52+
name: `${entry!.pallet}.${entry!.name}(…)`,
53+
promise,
54+
type: entry!.output,
55+
})
56+
}
57+
58+
return (
59+
<div className="flex flex-col gap-4 items-start w-full">
60+
<ViewFnInputValues />
61+
<ActionButton disabled={!isReady} onClick={submit}>
62+
Call
63+
</ActionButton>
64+
</div>
65+
)
66+
}
67+
68+
const [inputValueChange$, setInputValue] = createSignal<{
69+
idx: number
70+
value: Uint8Array | "partial" | null
71+
}>()
72+
const inputValues$ = selectedEntry$.pipeState(
73+
filter((v) => !!v),
74+
map((v) => v.inputs),
75+
switchMap((inputs) => {
76+
const values: Array<Uint8Array | "partial" | null> = inputs.map(() => null)
77+
return inputValueChange$.pipe(
78+
scan((acc, change) => {
79+
const newValue = [...acc]
80+
newValue[change.idx] = change.value
81+
return newValue
82+
}, values),
83+
startWith(values),
84+
)
85+
}),
86+
withDefault([] as Array<Uint8Array | "partial" | null>),
87+
)
88+
89+
const isReady$ = inputValues$.pipeState(
90+
map((inputValues) => inputValues.every((v) => v instanceof Uint8Array)),
91+
withDefault(false),
92+
)
93+
94+
const ViewFnInputValues: FC = () => {
95+
const selectedEntry = useStateObservable(selectedEntry$)
96+
if (!selectedEntry || !selectedEntry.inputs.length) return null
97+
98+
return (
99+
<div className="w-full">
100+
Inputs
101+
<ol className="flex flex-col gap-2">
102+
{selectedEntry.inputs.map((input, idx) => (
103+
<RuntimeValueInput
104+
key={idx}
105+
idx={idx}
106+
name={input.name}
107+
type={input.type}
108+
/>
109+
))}
110+
</ol>
111+
</div>
112+
)
113+
}
114+
115+
const inputValue$ = state(
116+
(idx: number) => inputValues$.pipe(map((v) => v[idx])),
117+
null,
118+
)
119+
const lookupState$ = state(lookup$, null)
120+
const RuntimeValueInput: FC<{ idx: number; name: string; type: number }> = ({
121+
idx,
122+
type,
123+
name,
124+
}) => {
125+
const value = useStateObservable(inputValue$(idx))
126+
const [expanded, setExpanded] = useState(false)
127+
const lookup = useStateObservable(lookupState$)
128+
129+
if (!lookup) return null
130+
const shape = lookup(type)
131+
const complexity = getTypeComplexity(shape)
132+
133+
return (
134+
<li key={idx} className="border rounded p-2 w-full">
135+
<div
136+
className={twMerge(
137+
"flex items-center select-none",
138+
complexity !== "inline" && "cursor-pointer",
139+
)}
140+
onClick={() => setExpanded((e) => !e)}
141+
>
142+
{complexity === "inline" ? (
143+
<Dot size={16} />
144+
) : (
145+
<ExpandBtn expanded={expanded} />
146+
)}
147+
<Circle
148+
size={8}
149+
strokeWidth={4}
150+
className={twMerge(
151+
"mr-1",
152+
value === null
153+
? "text-red-600"
154+
: value === "partial"
155+
? "text-orange-600"
156+
: "text-green-600",
157+
)}
158+
/>
159+
<div className="text-foreground/80">{name}</div>
160+
{complexity === "inline" ? (
161+
<div className="px-2">
162+
<InlineLookupTypeEdit
163+
type={type}
164+
value={value}
165+
onValueChange={(value) => setInputValue({ idx, value })}
166+
/>
167+
</div>
168+
) : null}
169+
</div>
170+
{expanded && complexity !== "inline" && (
171+
<div className="py-2 max-h-[60svh] overflow-hidden flex flex-col justify-stretch">
172+
<LookupTypeEdit
173+
type={type}
174+
value={value}
175+
onValueChange={(value) => setInputValue({ idx, value })}
176+
tree={complexity === "tree"}
177+
/>
178+
</div>
179+
)}
180+
</li>
181+
)
182+
}

0 commit comments

Comments
 (0)