Typesafe React hooks for seamless ink! smart contract interactions, powered by Dedot!
- Fully typesafe react hooks at contract messages & events level
- Choose your favorite wallet connector (Built-in Typink Connector, SubConnect, Talisman Connect, or build your own connector ...)
- Start a new project from scratch easily with
create-typink
cli. - Support multiple networks, lazily initialize when in-use.
- ... and more to come
# via npm
npm i typink dedot
# via yarn
yarn add typink dedot
# via pnpm
pnpm add typink dedot
Typink heavily uses Typescript to enable & ensure type-safety, so we recommend using Typescript for your Dapp project. Typink will also work with plain Javascript, but you don't get the auto-completion & suggestions when interacting with your ink! contracts.
Typink comes with a cli to help you start a new project from scratch faster & easier, to create a new project, run the below command:
npx create-typink@latest
Important
The create-typink
cli requires NodeJS version >= v20
to work properly, make sure to check your NodeJS version.
Following the instructions, the cli will help you generate a starter & working project ready for you to start integrate your own contracts and build your own logic:
![new-typink-project](https://private-user-images.githubusercontent.com/6867026/406999333-b10b1366-f97b-41a7-b3e9-97ceb1bd0748.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk0MzUzNTEsIm5iZiI6MTczOTQzNTA1MSwicGF0aCI6Ii82ODY3MDI2LzQwNjk5OTMzMy1iMTBiMTM2Ni1mOTdiLTQxYTctYjNlOS05N2NlYjFiZDA3NDgucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIxMyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMTNUMDgyNDExWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ODM0OThmNDE1YWM1NDFkZjcyNTRmMTQwMzgyNzlhYjc1MjVlNDczNjM0YWRkZmI3ZDMyNDczY2JlZTgxMTRiYSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.wK4iZ_gi0ARmC3vsRa3JF1xVqPdo7qpf19Xq0zwEdGk)
After initialize the project, you can now spin up the development server with the following command:
cd my-ink-dapp # project folder
yarn start
Important
Please note that yarn
is the current default package manager for the start project, make sure to install yarn
on your machine to streamline the development process.
contracts
: ink! contract artifacts & generated typescontracts/artifacts
: ink! contract artifacts (.wasm, .json or .contract file)contracts/types
: Typescript bindings for each ink! contract, these can be generated throughdedot
cli
ui
: Main UI project, a React-based client
Typink needs to know your contract deployments information (address, metadata, ...) to help you do the magic under the hood.
The ContractDeployment
interface have the following structure:
export interface ContractDeployment {
id: string; // A unique easy-to-remember contract id, recommened put them in an enum
metadata: ContractMetadata | string; // Metadata for the contract
address: SubstrateAddress; // Address of the contract
network: string; // The network id that the contract is deployed to, e.g: Pop Testnet (pop_testnet), Aleph Zero Testnet (alephzero_testnet) ...
}
Put these information in a separate file (e.g: deployments.ts
) so you can easily manage it.
// deployments.ts
import { ContractDeployment, popTestnet } from 'typink';
import greeterMetadata from './greeter.json';
import psp22Metadata from './psp22.json';
export enum ContractId {
GREETER = 'greeter',
PSP22 = 'psp22',
}
export const deployments = [
{
id: ContractId.GREETER,
metadata: greeterMetadata,
address: '5HJ2XLhBuoLkoJT5G2MfMWVpsybUtcqRGWe29Fo26JVvDCZG',
network: popTestnet.id
},
{
id: ContractId.PSP22,
metadata: psp22Metadata as any,
address: '16119BccKAfWwbt4TCNvfLBDuRWHSeFozJELEcxFPVd11hnt',
network: popTestnet.id,
},
];
Now, you'll need to generate the Typescript bindings for your ink! contracts using dedot
cli. The types generated at this step will help enable the auto-completion & suggestions for your react hooks when interact with the contracts.
We recommend putting these types in a ./contracts/types
folder. Let's generate types for your greeter
& psp22
token contracts
npx dedot typink -m ./greeter.json -o ./contracts/types
npx dedot typink -m ./psp22.json -o ./contracts/types
After running the commands, the types will generated into the ./contracts/types
folder. You'll get the top-level type interface for greeter
& psp22
contracts as: GreeterContractApi
and Psp22ContractApi
.
Tip
It's a good practice to put these commands to a shortcut script in th package.json
file so you can easily regenerate these types again whenver you update the metadata for your contracts
{
// ...
"scripts": {
"typink": "npx dedot typink -m ./greeter.json -o ./contracts/types && npx dedot typink -m ./psp22.json -o ./contracts/types"
}
// ...
}
To regenerate the types again:
npm run typink
# or
yarn typink
# or
pnpm typink
Wrap your application component with TypinkProvider
.
import { popTestnet, development } from 'typink'
import { deployments } from './deployments';
// a default caller address when interact with ink! contract if there is no wallet is connected
const DEFAULT_CALLER = '5xxx...'
const SUPPORTED_NETWORKS = [popTestnet]; // alephZeroTestnet, ...
if (process.env.NODE_ENV === 'development') {
SUPPORTED_NETWORKS.push(development);
}
<TypinkProvider
deployments={deployments}
defaultCaller={DEFAULT_CALLER}
supportedNetworks={SUPPORTED_NETWORKS}
defaultNetworkId={popTestnet.id}
cacheMetadata={true}>
<MyAppComponent ... />
</TypinkProvider>
If you're using an external wallet connector like SubConnect or Talisman Connect, you will need to pass into the TypinkProvider
2 more props: connectedAccount
(InjectedAccouunt) & signer
(Signer) so Typink knows which account & signer to interact with the ink! contracts.
const { connectedAccount, signer } = ... // from subconnect or talisman-connect ...
<TypinkProvider
deployments={deployments}
defaultCaller={DEFAULT_CALLER}
supportedNetworks={SUPPORTED_NETWORKS}
defaultNetworkId={popTestnet.id}
cacheMetadata={true}
connectedAccount={connectedAccount}
signer={signer}
>
<MyAppComponent ... />
</TypinkProvider>
TypinkProvider
: A global provider for Typink DApps, it managed shared state internally so hooks and child components can access (accounts, signer, wallet connection, Dedot clients, contract deployments ...)
useTypink
: Give access to internal shared state managed byTypinkProvider
giving access to connected account, signer, clients, contract deployments ...useBalance
,useBalances
: Fetch native Substrate balances of an address or list of addresses, helpful for checking if the account has enough fund to making transactionsuseRawContract
: Create & manageContract
instance given its metadata & addressuseContract
: Create & manageContract
instance given its unique id from the registered contract deploymentsuseContractTx
: Provides functionality to sign and send transactions to a smart contract, and tracks the progress of the transaction.useContractQuery
: Help making a contract queryuseDeployer
: Create & manageContractDeployer
instance given its unique id from the registered contract deploymentsuseDeployerTx
: Similar touseContractTx
, this hook provides functionality to sign and send transactions to deploy a smart contract, and tracks the progress of the transaction.useWatchContractEvent
: Help watch for a specific contract event and perform a specific actionusePSP22Balance
: Fetch balance of an address from a PSP22 contract with ability to watch for balance changing
formatBalance
: Format a balance value to a human-readable string
Access various shared states via useTypink
// ...
const {
accounts, // list available accounts connected from the wallet
connectedAccount, // connected account to interact with contracts & networks
network, // current connected network info
client, // Dedot clients to interact with network
deployments, // contract deployments
connectedWallet, // connected wallet
connectWallet, // func to connect to a wallet given its id
disconnect, // func to sign out and disconnect from the wallet
wallets, // available wallets
...
} = useTypink();
// ...
Instantiate a Greeter Contract
instance using useContract
and fetching the greet message using useContractQuery
// ...
import { GreeterContractApi } from '@/contracts/types/greeter';
const { contract } = useContract<GreeterContractApi>(ContractId.GREETER);
const {
data: greet,
isLoading,
refresh,
} = useContractQuery({
contract,
fn: 'greet',
});
// ...
Send a message to update the greeting message using useContractTx
// ...
import { GreeterContractApi } from '@/contracts/types/greeter';
const [message, setMessage] = useState('');
const { contract } = useContract<GreeterContractApi>(ContractId.GREETER);
const setMessageTx = useContractTx(contract, 'setMessage');
const doSetMessage = async () => {
if (!contract || !message) return;
try {
await setMessageTx.signAndSend({
args: [message],
callback: ({ status }) => {
console.log(status);
if (status.type === 'BestChainBlockIncluded') {
setMessage(''); // Reset the message if the transaction is in block
}
// TODO showing a toast notifying transaction status
},
});
} catch (e: any) {
console.error('Fail to make transaction:', e);
// TODO showing a toast message
}
}
// ...
Instantiate a ContractDeployer
instance to deploy Greeter contract using useDeployer
and deploying the contract using useDeployerTx
// ...
import { greeterMetadata } from '@/contracts/deployments.ts';
const wasm = greeterMetadata.source.wasm; // or greeterMetadata.source.hash (wasm hash code)
const { deployer } = useDeployer<GreeterContractApi>(greeterMetadata as any, wasm);
const newGreeterTx = useDeployerTx(deployer, 'new');
const [initMessage, setInitMessage] = useState<string>('');
const deployContraact = async () => {
if (!contract || !initMessage) return;
try {
// a random salt to make sure we don't get into duplicated contract deployments issue
const salt = numberToHex(Date.now());
await newGreeterTx.signAndSend({
args: [initMessage],
txOptions: { salt },
callback: ({ status }, deployedContractAddress) => {
console.log(status);
if (status.type === 'BestChainBlockIncluded') {
setInitMessage('');
}
if (deployedContractAddress) {
console.log('Contract is deployed at address', deployedContractAddress);
}
// TODO showing a toast notifying transaction status
},
});
} catch (e: any) {
console.error('Fail to make transaction:', e);
// TODO showing a toast message
}
}
// ...
Watching for the Greeted
event emitted
// ...
const { contract } = useContract<GreeterContractApi>(ContractId.GREETER);
useWatchContractEvent(
contract,
'Greeted', // fully-typed event name with auto-completion
useCallback((events) => {
events.forEach((greetedEvent) => {
const {
name,
data: { from, message },
} = greetedEvent; // fully-typed events
console.log(`Found a ${name} event sent from: ${from?.address()}, message: ${message}`);
});
}, []),
)
// ...
Format a balance value to a human-readable string
import { popTestnet } from 'typink';
formatBalance(1e12, popTestnet); // 100 PAS
- Demo (https://typink-demo.netlify.app/)
- Demo with SubConnect (https://typink-subconnect.netlify.app/)
Funded by W3F