Type-Safe TypeScript Interfaces & React Hooks for Aptos.
- No code-generation: Interact with smart contracts using fully typed APIs based on static type inference. Inspired by Viem.
- Get rid of encoding/decoding: Surf takes care of the complexities within the APIs, so you don't have to.
- Linting and Auto-Completion: Enjoy a better development experience with type safety. No more guesswork for input and output.
- Both TypeScript Interfaces & React Hooks: Easy to use, whether working with wallets or private keys.
- Low runtime cost & small bundle size: minzipped size: < 2 kB.
const client = createSurfClient(new Aptos());
const result = await client.useABI(COIN_ABI).entry.transfer({
functionArguments: ['0x1', 1],
typeArguments: ['0x1::aptos_coin::AptosCoin'],
account: Account.fromPrivateKey(...),
});
const [balance] = await client.useABI(COIN_ABI).view.balance({
functionArguments: ['0x1'],
typeArguments: ['0x1::aptos_coin::AptosCoin'],
});
When you input client.useABI(COIN_ABI).view.
into your IDE, the auto-completion show as below. You could also see the input and output types for the function, all of which are statically inferred from the ABI.
npm i @thalalabs/surf @aptos-labs/ts-sdk
If you want to use the React Hooks, install the @aptos-labs/wallet-adapter-react
additionally. Those React Hooks will be moved to a separate package in near future.
Create the client:
import { createSurfClient } from '@thalalabs/surf';
import { Aptos, Network, AptosConfig } from '@aptos-labs/ts-sdk';
const client = createSurfClient(new Aptos(
new AptosConfig({ network: Network.TESTNET })
));
Surf infers types from ABI to give you the end-to-end type-safety from your Move contract to your frontend. So firstly, you need to prepare the ABI json object of your contract in TypeScript (You can get ABI from Aptos Explorer, for example: 0x1::coin):
const abi = {…} as const;
If type inference isn't working, it's likely you forgot to add the const assertion for the object. And make sure that you set strict
to true
in your tsconfig.json
compiler options.
There are two ways to call a view function with the client:
// Option 1. Use the `useABI` interface
const [balance] = await client.useABI(COIN_ABI).view.balance({
functionArguments: ['0x1'],
typeArguments: ['0x1::aptos_coin::AptosCoin'],
ledgerVersion: '562606728', // ledger_version is optional
});
// Option 2. Create payload and use the `view` interface
import { createViewPayload } from '@thalalabs/surf';
const payload = createViewPayload(COIN_ABI, {
function: 'balance',
functionArguments: ['0x1'],
typeArguments: ['0x1::aptos_coin::AptosCoin'],
// (optional) you can also override the address in ABI
// address: "0x123"
});
const [balance] = await client.view({
payload,
options: { ledgerVersion: '562606728' }, // ledger_version is optional
});
Both of the interfaces can provide type safety.
Calling useABI
returns an object without any ABI parsing, so that the runtime cost is low.
Similar to the view
function, there are also two ways to submit transactions.
// prepare your AptosAccount
const account = /* your AptosAccount */;
// Option 1. Use the `useABI` interface
const { hash } = await client.useABI(COIN_ABI).entry.transfer({
functionArguments: ['0x1', 1],
typeArguments: ['0x1::aptos_coin::AptosCoin'],
account,
});
// Option 2. Create payload and use the `submitTransaction` interface
import { createEntryPayload } from "@thalalabs/surf";
const payload = createEntryPayload(COIN_ABI, {
function: 'transfer',
functionArguments: ['0x1', 1],
typeArguments: ['0x1::aptos_coin::AptosCoin'],
// (optional) you can also override the address in ABI
// address: "0x123"
});
const result = await client.submitTransaction({
payload,
signer: account,
});
You can also simulate a transaction:
// prepare your AptosAccount
const account = /* your AptosAccount */;
// Option 1. Use the `useABI` interface
const { hash } = await client.useABI(COIN_ABI).entry.transfer({
arguments: ['0x1', 1],
type_arguments: ['0x1::aptos_coin::AptosCoin'],
account,
isSimulation: true,
});
// Option 2. Create payload and use the `simulateTransaction` interface
import { createEntryPayload } from "@thalalabs/surf";
const entryPayload = createEntryPayload(COIN_ABI, {
function: 'transfer',
arguments: ['0x1', 1],
type_arguments: ['0x1::aptos_coin::AptosCoin'],
});
const { hash } = await client.simulateTransaction({
payload,
sender: account.accountAddress,
publicKey: account.publicKey,
});
To get account resource with type safety:
const { data } = await client.useABI(COIN_ABI).resource.CoinStore({
typeArguments: ['0x1::aptos_coin::AptosCoin'],
account: '0x1',
});
// Get property in the struct with type safety
console.log(data.frozen);
// It also works for nested struct type.
// The `coin` property's type is 0x1::coin::Coin<T0>
console.log(data.coin.value);
Some fields of a stuct may reference external modules.To inference the type of a nested struct, it needs the ABI of the external module. Surf currently only built-in some of the ABIs from 0x1, so that it can inference types like 0x1::coin::Coin
. The type of an unidentifiable field would be object
. Developer can provide additional modules to Surf like this:
import { DefaultABITable } from "@thalalabs/surf";
import { createSurfClient } from '@thalalabs/surf';
import { Aptos } from '@aptos-labs/ts-sdk';
type ABITAble = [
...DefaultABITable,
...[
typeof FIXED_POINT64_ABI
]
];
const client = createSurfClient<ABITAble>(new Aptos());
With this customized ABITAble
, Surf can inference the struct from 0x4dcae85fc5559071906cd5c76b7420fcbb4b0a92f00ab40ffc394aadbbff5ee9::fixed_point64
.
Considering the ABITable
is only been used as a type, it would be stripped out after compiling. So it won't increase the bundle size. You can put the ABITable
in a separate file and import type { ABITable } from "./ABITable.ts"
to ensure that.
Surf currently offers two React Hooks: useWalletClient
and useSubmitTransaction
. Both require the @aptos-labs/wallet-adapter-react
. Check out the example NextJS package for more information.
Surf support some special types like 0x1::object::Object
, 0x1::option::Option
. Aptos has specific rule for these types. For example, Aptos accepts hex strings as input for 0x1::object::Object
argument type.
Learning more backgrounds and design details from this blog post.
Compared to Viem, Surf is still in its infancy. Any contribution is welcome and appreciated. Here are some TODOs:
- Deploy a dedicated smart contract on the testnet for Surf to run tests that cover all data types. Currently, Surf has some tests running in CI, but they do not cover all types.
- Support
struct
types for return values forview
function. - Accept
Uint8Array
andstring
forvector<u8>
input. Currently users can pass these values tocreateEntryPayload
, and Surf will correctly encode it. But the type system will complain. So users need to useas any
to passUint8Array
orstring
forvector<u8>
. The type system only acceptnumber[]
forvector<u8>
now. - Add the functionality available in AptosClient to Surf, such as
estimateGasPrice
.
Released under MIT by @ThalaLabs.