Skip to content

Commit e2f2c5e

Browse files
feat: add example for LunoKit integration (#307)
* feat: add example for LunoKit integration * fix: fix the spelling errors in the TypinkApp Co-authored-by: Thang X. Vu <zthangxv@gmail.com> * fix: fix the spelling errors --------- Co-authored-by: Thang X. Vu <zthangxv@gmail.com>
1 parent 49b2fc8 commit e2f2c5e

46 files changed

Lines changed: 4617 additions & 14 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/demo-build-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ jobs:
3030
- run: yarn workspace demo-inkv5 build
3131
- run: yarn workspace demo-inkv6 build
3232
- run: yarn workspace demo-subconnect build
33+
- run: yarn workspace demo-lunokit build
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = {
2+
root: true,
3+
env: { browser: true, es2020: true },
4+
extends: [
5+
'eslint:recommended',
6+
'plugin:@typescript-eslint/recommended',
7+
'plugin:react-hooks/recommended',
8+
],
9+
ignorePatterns: ['dist', '.eslintrc.cjs'],
10+
parser: '@typescript-eslint/parser',
11+
plugins: ['react-refresh'],
12+
rules: {
13+
'react-refresh/only-export-components': [
14+
'warn',
15+
{ allowConstantExport: true },
16+
],
17+
"@typescript-eslint/no-explicit-any": "off"
18+
},
19+
}

examples/demo-lunokit/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Example Dapp
2+
3+
- Start the application by running:
4+
```shell
5+
# From examples/demo-lunokit folder
6+
yarn dev
7+
8+
# From the project root folder
9+
yarn workspace demo-lunokit dev
10+
```

examples/demo-lunokit/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Typink + LunoKit</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

examples/demo-lunokit/package.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "demo-lunokit",
3+
"private": true,
4+
"version": "0.0.1",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10+
"preview": "vite preview",
11+
"typink": "npx dedot typink -m ./src/contracts/artifacts/psp22/psp22.contract -o ./src/contracts/types && npx dedot typink -m ./src/contracts/artifacts/greeter/greeter.contract -o ./src/contracts/types"
12+
},
13+
"dependencies": {
14+
"@chakra-ui/icons": "2.2.5",
15+
"@chakra-ui/react": "^2.10.5",
16+
"@chakra-ui/system": "^2.6.2",
17+
"@emotion/react": "^11.14.0",
18+
"@emotion/styled": "^11.14.0",
19+
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
20+
"@luno-kit/react": "^0.0.8",
21+
"@luno-kit/ui": "^0.0.8",
22+
"@tanstack/react-query": "5.81.5",
23+
"dedot": "^0.18.3",
24+
"framer-motion": "^10.18.0",
25+
"react": "^19.1.1",
26+
"react-dom": "^19.1.1",
27+
"react-use": "^17.6.0",
28+
"sonner": "^2.0.7",
29+
"svelte-qrcode": "^1.0.1",
30+
"typink": "workspace:*"
31+
},
32+
"devDependencies": {
33+
"@dedot/chaintypes": "^0.161.0",
34+
"@types/react": "^19.1.1",
35+
"@types/react-dom": "^19.1.1",
36+
"@typescript-eslint/eslint-plugin": "^8.44.0",
37+
"@typescript-eslint/parser": "^8.44.0",
38+
"@vitejs/plugin-react": "^4.3.4",
39+
"eslint": "^9.35.0",
40+
"eslint-plugin-react-hooks": "^5.2.0",
41+
"eslint-plugin-react-refresh": "^0.4.20",
42+
"typescript": "^5.9.2",
43+
"vite": "^7.1.6",
44+
"vite-tsconfig-paths": "^5.1.4"
45+
}
46+
}
15 KB
Binary file not shown.
111 KB
Loading

examples/demo-lunokit/src/App.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
2+
import { useState } from 'react';
3+
import { useSearchParam } from 'react-use';
4+
import Psp22Board from '@/components/Psp22Board.tsx';
5+
import BalanceInsufficientAlert from '@/components/shared/BalanceInsufficientAlert.tsx';
6+
import MainFooter from '@/components/shared/MainFooter';
7+
import MainHeader from '@/components/shared/MainHeader';
8+
import GreetBoard from '@/components/GreeterBoard.tsx';
9+
10+
function App() {
11+
const tab = useSearchParam('tab');
12+
const tabIndex = tab ? parseInt(tab) : 0;
13+
const [index, setIndex] = useState(tabIndex);
14+
15+
const handleTabsChange = (index: number) => {
16+
setIndex(index);
17+
history.pushState({}, '', location.pathname + `?tab=${index}`);
18+
};
19+
20+
return (
21+
<Flex direction='column' minHeight='100vh'>
22+
<MainHeader />
23+
<Box maxWidth='760px' mx='auto' my={4} px={4} flex={1} w='full'>
24+
<BalanceInsufficientAlert />
25+
<Tabs index={index} onChange={handleTabsChange}>
26+
<TabList>
27+
<Tab>Greeter Contract</Tab>
28+
<Tab>PSP22 Contract</Tab>
29+
</TabList>
30+
31+
<TabPanels>
32+
<TabPanel>
33+
<GreetBoard />
34+
</TabPanel>
35+
<TabPanel>
36+
<Psp22Board />
37+
</TabPanel>
38+
</TabPanels>
39+
</Tabs>
40+
</Box>
41+
<MainFooter />
42+
</Flex>
43+
);
44+
}
45+
46+
export default App;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Box, Button, Flex, FormControl, FormHelperText, FormLabel, Heading, Input, Text } from '@chakra-ui/react';
2+
import { useCallback, useState } from 'react';
3+
import { toast } from 'sonner';
4+
import PendingText from '@/components/shared/PendingText.tsx';
5+
import { shortenAddress } from '@/utils/string.ts';
6+
import { ContractId } from 'contracts/deployments';
7+
import { GreeterContractApi } from 'contracts/types/greeter';
8+
import { txToaster, useContract, useContractQuery, useContractTx, useWatchContractEvent } from 'typink';
9+
10+
export default function GreetBoard() {
11+
const { contract } = useContract<GreeterContractApi>(ContractId.GREETER);
12+
const [message, setMessage] = useState('');
13+
const setMessageTx = useContractTx(contract, 'setMessage');
14+
15+
const { data: greet, isLoading } = useContractQuery({
16+
contract,
17+
fn: 'greet',
18+
watch: true,
19+
});
20+
21+
const handleUpdateGreeting = async () => {
22+
if (!contract || !message) return;
23+
24+
const toaster = txToaster('Signing transaction...');
25+
26+
try {
27+
await setMessageTx.signAndSend({
28+
args: [message],
29+
callback: (progress) => {
30+
const { status } = progress;
31+
console.log(status);
32+
33+
if (status.type === 'BestChainBlockIncluded') {
34+
setMessage('');
35+
}
36+
37+
toaster.onTxProgress(progress);
38+
},
39+
});
40+
} catch (e: any) {
41+
console.error(e, e.message);
42+
toaster.onTxError(e);
43+
}
44+
};
45+
46+
// Listen to Greeted event from system events
47+
// & update the greeting message in real-time
48+
//
49+
// To verify this, try open 2 tabs of the app
50+
// & update the greeting message in one tab,
51+
// you will see the greeting message updated in the other tab
52+
useWatchContractEvent(
53+
contract,
54+
'Greeted',
55+
useCallback((events) => {
56+
events.forEach((greetedEvent) => {
57+
const {
58+
name,
59+
data: { from, message },
60+
} = greetedEvent;
61+
62+
console.log(`Found a ${name} event sent from: ${from?.address()}, message: ${message} `);
63+
64+
toast.info(
65+
<div>
66+
<p>
67+
Found a <b>{name}</b> event
68+
</p>
69+
<p style={{ fontSize: 12 }}>
70+
Sent from: <b>{shortenAddress(from?.address())}</b>
71+
</p>
72+
<p style={{ fontSize: 12 }}>
73+
Greeting message: <b>{message}</b>
74+
</p>
75+
</div>,
76+
);
77+
});
78+
}, []),
79+
);
80+
81+
return (
82+
<Box>
83+
<Heading size='md'>Greeter Contract</Heading>
84+
<Flex my={4} gap={2}>
85+
<Text>Greeting Message:</Text>
86+
<PendingText fontWeight='600' isLoading={isLoading} color='primary.500'>
87+
{greet}
88+
</PendingText>
89+
</Flex>
90+
<form>
91+
<FormControl>
92+
<FormLabel>Update greeting message:</FormLabel>
93+
<Input
94+
type='input'
95+
maxLength={50}
96+
value={message}
97+
onChange={(e) => setMessage(e.target.value)}
98+
isDisabled={setMessageTx.inBestBlockProgress}
99+
/>
100+
<FormHelperText>Max 50 characters</FormHelperText>
101+
</FormControl>
102+
<Button
103+
size='sm'
104+
mt={4}
105+
isDisabled={!message}
106+
isLoading={setMessageTx.inBestBlockProgress}
107+
onClick={handleUpdateGreeting}>
108+
Update Greeting
109+
</Button>
110+
</form>
111+
</Box>
112+
);
113+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Box, Button, Divider, Heading } from '@chakra-ui/react';
2+
import PendingText from '@/components/shared/PendingText.tsx';
3+
import { ContractId } from 'contracts/deployments';
4+
import { Psp22ContractApi } from 'contracts/types/psp22';
5+
import { formatBalance, txToaster, useContract, useContractQuery, useContractTx, useTypink } from 'typink';
6+
import { useStatus } from '@luno-kit/react';
7+
import { ConnectButton } from '@luno-kit/ui';
8+
9+
export default function Psp22Board() {
10+
const { contract } = useContract<Psp22ContractApi>(ContractId.PSP22);
11+
const { defaultCaller, connectedAccount, network } = useTypink();
12+
const mintTx = useContractTx(contract, 'psp22MintableMint');
13+
const connectionStatus = useStatus()
14+
15+
const { data: tokenName, isLoading: loadingTokenName } = useContractQuery({
16+
contract,
17+
fn: 'psp22MetadataTokenName',
18+
});
19+
20+
const { data: tokenSymbol, isLoading: loadingTokenSymbol } = useContractQuery({
21+
contract,
22+
fn: 'psp22MetadataTokenSymbol',
23+
});
24+
25+
const { data: tokenDecimal, isLoading: loadingTokenDecimal } = useContractQuery({
26+
contract,
27+
fn: 'psp22MetadataTokenDecimals',
28+
});
29+
30+
const {
31+
data: totalSupply,
32+
isLoading: loadingTotalSupply,
33+
refresh: refreshTotalSupply,
34+
} = useContractQuery({
35+
contract,
36+
fn: 'psp22TotalSupply',
37+
});
38+
39+
const {
40+
data: myBalance,
41+
isLoading: loadingBalance,
42+
refresh: refreshMyBalance,
43+
} = useContractQuery({
44+
contract,
45+
fn: 'psp22BalanceOf',
46+
args: [connectedAccount?.address || defaultCaller],
47+
});
48+
49+
const mintNewToken = async () => {
50+
if (!tokenDecimal) return;
51+
52+
const toaster = txToaster('Signing transaction...');
53+
try {
54+
await mintTx.signAndSend({
55+
args: [BigInt(100 * Math.pow(10, tokenDecimal))],
56+
callback: (progress) => {
57+
const { status } = progress;
58+
console.log(status);
59+
60+
if (status.type === 'BestChainBlockIncluded') {
61+
refreshMyBalance();
62+
refreshTotalSupply();
63+
}
64+
65+
toaster.onTxProgress(progress);
66+
},
67+
});
68+
} catch (e: any) {
69+
console.error(e);
70+
toaster.onTxError(e);
71+
} finally {
72+
refreshMyBalance();
73+
refreshTotalSupply();
74+
}
75+
};
76+
77+
return (
78+
<Box>
79+
<Heading size='md'>PSP22 Contract</Heading>
80+
<Box mt={4}>
81+
<Box mb={2}>
82+
Token Name:{' '}
83+
<PendingText fontWeight='600' isLoading={loadingTokenName}>
84+
{tokenName}
85+
</PendingText>
86+
</Box>
87+
<Box mb={2}>
88+
Token Symbol:{' '}
89+
<PendingText fontWeight='600' isLoading={loadingTokenSymbol}>
90+
{tokenSymbol}
91+
</PendingText>
92+
</Box>
93+
<Box mb={2}>
94+
Token Decimal:{' '}
95+
<PendingText fontWeight='600' isLoading={loadingTokenDecimal}>
96+
{tokenDecimal}
97+
</PendingText>
98+
</Box>
99+
<Box mb={2}>
100+
Total Supply:{' '}
101+
<PendingText fontWeight='600' isLoading={loadingTotalSupply}>
102+
{formatBalance(totalSupply, network)}
103+
</PendingText>
104+
</Box>
105+
<Divider my={4} />
106+
<Box>
107+
My Balance:{' '}
108+
{connectionStatus === 'connected' ? (
109+
<PendingText fontWeight='600' isLoading={loadingBalance}>
110+
{formatBalance(myBalance, network)}
111+
</PendingText>
112+
) : (
113+
<ConnectButton />
114+
)}
115+
</Box>
116+
{connectedAccount && (
117+
<Box mt={4}>
118+
<Button size='sm' onClick={mintNewToken} isLoading={mintTx.inBestBlockProgress}>
119+
Mint 100 {tokenSymbol}
120+
</Button>
121+
</Box>
122+
)}
123+
</Box>
124+
</Box>
125+
);
126+
}

0 commit comments

Comments
 (0)