Typesafe react hooks to interact with ink! smart contracts 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 ...)
- 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.
Note
Starting a new project from scratch?
Try out the prerelease of typink dapp template: https://github.com/dedotdev/typink-dapp-template.
Clone the repo & have fun!
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 queryuseWatchContractQuery
: Similar touseContractQuery
with ability to watch for changesuseDeployer
: 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
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}`);
});
}, []),
)
// ...
- Demo (https://typink-demo.netlify.app/)
- Demo with SubConnect (https://typink-subconnect.netlify.app/)
Funded by W3F