Skip to content

Commit 9cc6d15

Browse files
authored
feat: implement sample dapp calling oracle (#270)
1 parent b83f9f4 commit 9cc6d15

File tree

15 files changed

+389
-95
lines changed

15 files changed

+389
-95
lines changed

apps/sample-dapp/blockchain/contracts/App.sol

-8
This file was deleted.

apps/sample-dapp/blockchain/contracts/Callback.sol

+11-2
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@ pragma solidity ^0.8.9;
44
import 'hardhat/console.sol';
55

66
contract Callback {
7+
uint256 public intAnswer;
8+
string public strAnswer;
9+
710
function receiveNumericAnswer(
811
uint256 _questionId,
912
uint256 _finalAnswer
1013
) public {
14+
intAnswer = _finalAnswer;
1115
console.log('receiveNumericAnswer: ', _questionId, _finalAnswer);
1216
}
1317

1418
function receiveStringAnswer(
1519
uint256 _questionId,
1620
string memory _finalAnswer
1721
) public {
22+
strAnswer = _finalAnswer;
1823
console.log('receiveStringAnswer: ', _questionId, _finalAnswer);
1924
}
2025

21-
function receiveAnswer(string memory res) public {
22-
console.log('Received from oracle: %s', res);
26+
function getNumericAnswer() public view returns (uint256) {
27+
return intAnswer;
28+
}
29+
30+
function getStringAnswer() public view returns (string memory) {
31+
return strAnswer;
2332
}
2433
}

apps/sample-dapp/blockchain/scripts/deploy.ts

-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ async function main() {
99
const CallbackContract = await ethers.getContractFactory('Callback');
1010
const callbackContract = await CallbackContract.deploy();
1111
console.log('CallbackContract deployed to:', callbackContract.address);
12-
const AppContract = await ethers.getContractFactory('App');
13-
const appContract = await AppContract.deploy();
14-
console.log('AppContract deployed to:', appContract.address);
1512

1613
const rawEnvData = dotenv.parse(fs.readFileSync(envFilePath));
1714
rawEnvData.VITE_CALLBACK_CONTRACT_ADDRESS = callbackContract.address;
-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
VITE_ORACLE_CONTRACT_ADDRESS=
2-
VITE_APP_CONTRACT_ADDRESS=
32
VITE_CALLBACK_CONTRACT_ADDRESS=

apps/sample-dapp/frontend/.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@
2323
"react": {
2424
"version": "detect"
2525
}
26-
}
26+
},
27+
"ignorePatterns": ["src/components/ui/*"]
2728
}

apps/sample-dapp/frontend/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
},
1616
"dependencies": {
1717
"@radix-ui/react-icons": "^1.3.0",
18+
"@radix-ui/react-separator": "^1.0.3",
1819
"@radix-ui/react-slot": "^1.0.2",
20+
"@radix-ui/react-tabs": "^1.0.4",
1921
"class-variance-authority": "^0.7.0",
2022
"clsx": "^2.0.0",
23+
"lucide-react": "^0.279.0",
2124
"react": "^18.2.0",
2225
"react-dom": "^18.2.0",
26+
"sonner": "^1.0.3",
2327
"tailwind-merge": "^1.14.0",
2428
"tailwindcss-animate": "^1.0.7",
2529
"web3": "^1.9.0"

apps/sample-dapp/frontend/src/app.tsx

+122-42
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,144 @@
11
import { useState, useEffect } from 'react';
22
import { useOracle } from './hooks/use-oracle';
3-
import { useBlockchain } from './hooks/use-blockchain';
3+
import { Input } from './components/ui/input';
4+
5+
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
6+
import { Button } from './components/ui/button';
7+
import { Loader2, Send, Terminal } from 'lucide-react';
8+
import { toast } from 'sonner';
9+
import { Separator } from './components/ui/separator';
10+
11+
import { Alert, AlertDescription, AlertTitle } from './components/ui/alert';
412

513
const AppPage = () => {
6-
const { askOracle, subscribeOracle } = useOracle();
7-
const { getData } = useBlockchain();
8-
const [question, setQuestion] = useState<string>('');
9-
const [callbackAddress, setCallbackAddress] = useState<string>('');
14+
const { askOracle, getStringResponse, getNumericResponse } = useOracle();
15+
const [stringQuestion, setStringQuestion] = useState<string>('');
16+
const [numericQuestion, setNumericQuestion] = useState<string>('');
17+
const [stringResponse, setStringResponse] = useState<string | null>(null);
18+
const [numericResponse, setNumericResponse] = useState<number | null>(null);
19+
const [loading, setLoading] = useState<boolean>(false);
1020

11-
const handleSubmit = async () => {
21+
const submitStringQuestion = async () => {
22+
setLoading(true);
23+
if (!stringQuestion) {
24+
toast.error('請輸入問題');
25+
setLoading(false);
26+
return;
27+
}
1228
try {
1329
await askOracle({
1430
dataType: 'String',
15-
question: question,
16-
callBackAddress: callbackAddress,
31+
question: stringQuestion,
32+
});
33+
toast.success('已將問題提交給 Oracle');
34+
setStringQuestion('');
35+
} catch (error) {
36+
console.error('oracle error:', error);
37+
toast.error('Oracle 發生錯誤');
38+
}
39+
setLoading(false);
40+
};
41+
42+
const submitNumericQuestion = async () => {
43+
setLoading(true);
44+
if (!numericQuestion) {
45+
toast.error('請輸入問題');
46+
setLoading(false);
47+
return;
48+
}
49+
try {
50+
await askOracle({
51+
dataType: 'Numeric',
52+
question: numericQuestion,
1753
});
18-
console.log('Ask oracle success!');
54+
toast.success('已將問題提交給 Oracle');
55+
setNumericQuestion('');
1956
} catch (error) {
2057
console.error('oracle error:', error);
58+
toast.error('Oracle 發生錯誤');
2159
}
60+
setLoading(false);
2261
};
2362

2463
useEffect(() => {
25-
subscribeOracle();
26-
const fetchData = async () => {
27-
const res = await getData();
28-
console.log(res);
64+
const fetchOracleResponse = async () => {
65+
const stringResponse = await getStringResponse();
66+
const numericResponse = await getNumericResponse();
67+
setStringResponse(stringResponse);
68+
setNumericResponse(numericResponse);
2969
};
30-
fetchData();
70+
fetchOracleResponse();
3171
}, []);
3272

3373
return (
34-
<div className="bg-slate-50 h-screen w-screen items-center justify-center flex">
35-
<div className="gap-6 flex flex-col w-1/2">
36-
<h1 className="text-3xl font-semibold text-slate-900">Sample DApp</h1>
37-
<div>
38-
<input
39-
className="h-12 w-full rounded-lg border-slate-300 border px-4"
40-
placeholder="請輸入問題"
41-
value={question}
42-
onChange={(e) => setQuestion(e.target.value)}
43-
/>
44-
<input
45-
className="h-12 w-full rounded-lg border-slate-300 border px-4"
46-
placeholder="請輸入回調地址"
47-
value={callbackAddress}
48-
onChange={(e) => setCallbackAddress(e.target.value)}
49-
/>
50-
</div>
51-
<div>
52-
<button
53-
className="border-slate-300 border rounded-lg px-3 py-2"
54-
onClick={handleSubmit}
55-
>
56-
送出
57-
</button>
58-
</div>
59-
<div>
60-
<p>Oracle 回應:</p>
61-
</div>
74+
<div className="container">
75+
<h1 className="text-2xl font-semibold mt-12">Sample DApp</h1>
76+
<Tabs defaultValue="string" className="mt-6">
77+
<TabsList>
78+
<TabsTrigger value="string">字串問題</TabsTrigger>
79+
<TabsTrigger value="numeric">數字問題</TabsTrigger>
80+
</TabsList>
81+
<TabsContent value="string">
82+
<h2 className="mt-4 text-lg">請輸入字串問題</h2>
83+
<div className="flex mt-2 space-x-2">
84+
<Input
85+
placeholder="請輸入問題"
86+
value={stringQuestion}
87+
onChange={(e) => setStringQuestion(e.target.value)}
88+
/>
89+
<Button
90+
className="flex-shrink-0"
91+
onClick={submitStringQuestion}
92+
disabled={loading}
93+
>
94+
{loading ? (
95+
<Loader2 className="animate-spin h-4 w-4 mr-2" />
96+
) : (
97+
<Send className="h-4 w-4 mr-2" />
98+
)}
99+
{loading ? '送出中' : '送出'}
100+
</Button>
101+
</div>
102+
</TabsContent>
103+
<TabsContent value="numeric">
104+
<h2 className="mt-4 text-lg">請輸入數字問題</h2>
105+
<div className="flex mt-2 space-x-2">
106+
<Input
107+
placeholder="請輸入問題"
108+
value={numericQuestion}
109+
onChange={(e) => setNumericQuestion(e.target.value)}
110+
/>
111+
<Button
112+
className="flex-shrink-0"
113+
onClick={submitNumericQuestion}
114+
disabled={loading}
115+
>
116+
{loading ? (
117+
<Loader2 className="animate-spin h-4 w-4 mr-2" />
118+
) : (
119+
<Send className="h-4 w-4 mr-2" />
120+
)}
121+
{loading ? '送出中' : '送出'}
122+
</Button>
123+
</div>
124+
</TabsContent>
125+
</Tabs>
126+
<Separator className="my-8" />
127+
<div className="flex space-x-3">
128+
<Alert>
129+
<Terminal className="h-4 w-4" />
130+
<AlertTitle>Oracle 字串回應</AlertTitle>
131+
<AlertDescription>
132+
{stringResponse ?? '等待 Oracle 回應中'}
133+
</AlertDescription>
134+
</Alert>
135+
<Alert>
136+
<Terminal className="h-4 w-4" />
137+
<AlertTitle>Oracle 數字回應</AlertTitle>
138+
<AlertDescription>
139+
{numericResponse ?? '等待 Oracle 回應中'}
140+
</AlertDescription>
141+
</Alert>
62142
</div>
63143
</div>
64144
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from 'react';
2+
import { cva, type VariantProps } from 'class-variance-authority';
3+
4+
import { cn } from '../../lib/utils';
5+
6+
const alertVariants = cva(
7+
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
8+
{
9+
variants: {
10+
variant: {
11+
default: 'bg-background text-foreground',
12+
destructive:
13+
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
14+
},
15+
},
16+
defaultVariants: {
17+
variant: 'default',
18+
},
19+
}
20+
);
21+
22+
const Alert = React.forwardRef<
23+
HTMLDivElement,
24+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25+
>(({ className, variant, ...props }, ref) => (
26+
<div
27+
ref={ref}
28+
role="alert"
29+
className={cn(alertVariants({ variant }), className)}
30+
{...props}
31+
/>
32+
));
33+
Alert.displayName = 'Alert';
34+
35+
const AlertTitle = React.forwardRef<
36+
HTMLParagraphElement,
37+
React.HTMLAttributes<HTMLHeadingElement>
38+
>(({ className, ...props }, ref) => (
39+
<h5
40+
ref={ref}
41+
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
42+
{...props}
43+
/>
44+
));
45+
AlertTitle.displayName = 'AlertTitle';
46+
47+
const AlertDescription = React.forwardRef<
48+
HTMLParagraphElement,
49+
React.HTMLAttributes<HTMLParagraphElement>
50+
>(({ className, ...props }, ref) => (
51+
<div
52+
ref={ref}
53+
className={cn('text-sm [&_p]:leading-relaxed', className)}
54+
{...props}
55+
/>
56+
));
57+
AlertDescription.displayName = 'AlertDescription';
58+
59+
export { Alert, AlertTitle, AlertDescription };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '../../lib/utils';
4+
5+
export interface InputProps
6+
extends React.InputHTMLAttributes<HTMLInputElement> {}
7+
8+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
9+
({ className, type, ...props }, ref) => {
10+
return (
11+
<input
12+
type={type}
13+
className={cn(
14+
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
15+
className
16+
)}
17+
ref={ref}
18+
{...props}
19+
/>
20+
);
21+
}
22+
);
23+
Input.displayName = 'Input';
24+
25+
export { Input };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
import * as SeparatorPrimitive from '@radix-ui/react-separator';
3+
4+
import { cn } from '../../lib/utils';
5+
6+
const Separator = React.forwardRef<
7+
React.ElementRef<typeof SeparatorPrimitive.Root>,
8+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9+
>(
10+
(
11+
{ className, orientation = 'horizontal', decorative = true, ...props },
12+
ref
13+
) => (
14+
<SeparatorPrimitive.Root
15+
ref={ref}
16+
decorative={decorative}
17+
orientation={orientation}
18+
className={cn(
19+
'shrink-0 bg-border',
20+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
21+
className
22+
)}
23+
{...props}
24+
/>
25+
)
26+
);
27+
Separator.displayName = SeparatorPrimitive.Root.displayName;
28+
29+
export { Separator };

0 commit comments

Comments
 (0)