Skip to content

Commit

Permalink
Support 0x1::option::Option as function inputs, and add some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
SamuelQZQ committed Jul 12, 2023
1 parent 907a914 commit a0b7dc2
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-pants-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@thalalabs/surf': patch
---

Support 0x1::option::Option as function inputs, and add some tests
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,17 @@ Considering the `ABITable` is only been used as a type, it would be stripped out

Surf currently offers two React Hooks: `useWalletClient` and `useSubmitTransaction`. Both require the `@aptos-labs/wallet-adapter-react`. Check out the [example NextJS package](https://github.com/ThalaLabs/surf/blob/main/example/app/page.tsx) for more information.

### Special types

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.

## TODOs
Compared to [Viem](https://viem.sh/), 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 for `view` function.
- [ ] Support vector of vector.
- [ ] Accept `Uint8Array` and `string` for `vector<u8>` input. Currently users can pass these values to `createEntryPayload`, and Surf will correctly encode it. But the type system will complain. So users need to use `as any` to pass `Uint8Array` or `string` for `vector<u8>`. The type system only accept `number[]` for `vector<u8>` now.
- [ ] Add the functionality available in AptosClient to Surf, such as `estimateGasPrice` and `getAccountResources`.
- [ ] Add the functionality available in AptosClient to Surf, such as `estimateGasPrice`.

## License

Expand Down
144 changes: 144 additions & 0 deletions src/core/__tests__/option.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* These test cases depends on network, it call the real contract.
*/

import { AptosAccount } from 'aptos';
import { createClient } from '../Client';
import { createViewPayload } from '../createViewPayload';
import { createEntryPayload } from '../createEntryPayload';

describe('option type', () => {
const client = createClient({
nodeUrl: 'https://fullnode.testnet.aptoslabs.com/v1',
});

const account = new AptosAccount(
undefined,
'0xac914efd2367c7aa42c95d100592c099e487d2270bf0e0761e5fe93ff4016593',
);

// TODO: correctly encode option type for view function
it('view function some value', async () => {
const payload = createViewPayload(OPTION_ABI, {
function: 'test_option_view',
arguments: [{ vec: ['50'] } as any],
type_arguments: [],
});
const result = await client.view(payload);
expect(result).toMatchInlineSnapshot(`
[
50n,
]
`);
}, 60000);

it('view function none value', async () => {
const payload = createViewPayload(OPTION_ABI, {
function: 'test_option_view',
arguments: [{ vec: [] } as any],
type_arguments: [],
});
const result = await client.view(payload);
expect(result).toMatchInlineSnapshot(`
[
0n,
]
`);
}, 60000);

it('entry function none value', async () => {
const payload = createEntryPayload(OPTION_ABI, {
function: 'test_option_entry',
arguments: [[]],
type_arguments: [],
});

const result = await client.simulateTransaction(payload, { account });

expect(result.hash).toBeDefined();
expect((result as any).payload).toMatchInlineSnapshot(`
{
"arguments": [
{
"vec": [],
},
],
"function": "0xf1ab5cd814ef1480b8c36466310d9c21d7758b54f6121872d1fb43887a40e7d8::test_option::test_option_entry",
"type": "entry_function_payload",
"type_arguments": [],
}
`);
}, 60000);

it('entry function some value', async () => {
const payload = createEntryPayload(OPTION_ABI, {
function: 'test_option_entry',
arguments: [[50]],
type_arguments: [],
});

const result = await client.simulateTransaction(payload, { account });

expect(result.hash).toBeDefined();
expect((result as any).payload).toMatchInlineSnapshot(`
{
"arguments": [
{
"vec": [
"50",
],
},
],
"function": "0xf1ab5cd814ef1480b8c36466310d9c21d7758b54f6121872d1fb43887a40e7d8::test_option::test_option_entry",
"type": "entry_function_payload",
"type_arguments": [],
}
`);
}, 60000);

it('entry function incorrect type', async () => {
// no need to run, only for type check
() => {
createEntryPayload(OPTION_ABI, {
function: 'test_option_entry',
// @ts-expect-error should be a vector
arguments: [{}],
type_arguments: [],
});

createEntryPayload(OPTION_ABI, {
function: 'test_option_entry',
// @ts-expect-error should be a vector with length 0 or 1
arguments: [[1, 2]],
type_arguments: [],
});
}
}, 60000);
});

const OPTION_ABI = {
address: '0xf1ab5cd814ef1480b8c36466310d9c21d7758b54f6121872d1fb43887a40e7d8',
name: 'test_option',
friends: [],
exposed_functions: [
{
name: 'test_option_entry',
visibility: 'public',
is_entry: true,
is_view: false,
generic_type_params: [],
params: ['0x1::option::Option<u64>'],
return: [],
},
{
name: 'test_option_view',
visibility: 'public',
is_entry: false,
is_view: true,
generic_type_params: [],
params: ['0x1::option::Option<u64>'],
return: ['u64'],
},
],
structs: [],
} as const;
12 changes: 12 additions & 0 deletions src/core/createEntryPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ function argToBCS(type: string, arg: any, serializer: BCS.Serializer) {
return;
}

const optionRegex = /0x1::option::Option<([^]+)>/;
const optionMatch = type.match(optionRegex);
if (optionMatch) { // It's 0x1::option::Option
const innerType = optionMatch[1]!;
serializer.serializeU32AsUleb128(arg.length);
if(!(arg instanceof Array) || arg.length > 1) {
throw new Error('Invalid input value for 0x1::option::Option.');
}
arg.forEach((arg) => argToBCS(innerType, arg, serializer));
return;
}

switch (
type // It's primitive
) {
Expand Down
2 changes: 2 additions & 0 deletions src/types/convertor/argsConvertor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ type ConvertNonStructArgType<TMoveType extends MoveNonStructTypes> =
? ConvertArgType<TInner>[]
: TMoveType extends `0x1::object::Object<${string}>`
? `0x${string}`
: TMoveType extends `0x1::option::Option<${infer TInner}>`
? ([ConvertArgType<TInner>] | [])
: UnknownStruct<TMoveType>;
36 changes: 17 additions & 19 deletions src/types/convertor/structConvertor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ export type ConvertStructFieldType<
TMoveType extends string,
> = TMoveType extends MoveNonStructTypes
? // it's a non-struct type
ConvertStructFieldNonStructType<TABITable, TMoveType>
: TMoveType extends `0x1::option::Option<${infer TInner}>`
? // it's 0x1::option::Option
ConvertStructFieldOptionType<TABITable, TInner>
ConvertStructFieldNonStructType<TABITable, TMoveType>
: // it's a struct type
ConvertStructFieldStructType<TABITable, TMoveType>;
ConvertStructFieldStructType<TABITable, TMoveType>;

/**
* Internal
Expand Down Expand Up @@ -53,6 +50,8 @@ type ConvertStructFieldNonStructType<
? ConvertPrimitiveStructField<TMoveType>
: TMoveType extends `vector<${infer TInner}>`
? ConvertStructFieldType<TABITable, TInner>[]
: TMoveType extends `0x1::option::Option<${infer TInner}>`
? ConvertStructFieldOptionType<TABITable, TInner>
: UnknownStruct<TMoveType>;

type ConvertStructFieldOptionType<
Expand All @@ -66,19 +65,18 @@ type ConvertStructFieldOptionType<
type ConvertStructFieldStructType<
TABITable extends ABITable,
TMoveType extends string,
> = TMoveType extends `${infer TAccountAddress}::${infer TModuleName}::${infer TStructName}${
| ''
| `<${infer _TInnerType}>`}`
> = TMoveType extends `${infer TAccountAddress}::${infer TModuleName}::${infer TStructName}${| ''
| `<${infer _TInnerType}>`}`
? `${TAccountAddress}::${TModuleName}` extends keyof TABITable
? OmitInner<TStructName> extends ResourceStructName<
TABITable[`${TAccountAddress}::${TModuleName}`]
>
? ExtractStructType<
TABITable,
TABITable[`${TAccountAddress}::${TModuleName}`],
OmitInner<TStructName>
>
: // Unknown struct, use the default struct type
UnknownStruct<TMoveType>
: UnknownStruct<TMoveType>
? OmitInner<TStructName> extends ResourceStructName<
TABITable[`${TAccountAddress}::${TModuleName}`]
>
? ExtractStructType<
TABITable,
TABITable[`${TAccountAddress}::${TModuleName}`],
OmitInner<TStructName>
>
: // Unknown struct, use the default struct type
UnknownStruct<TMoveType>
: UnknownStruct<TMoveType>
: UnknownStruct<TMoveType>;
4 changes: 3 additions & 1 deletion src/types/moveTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Types from Move language
*/

export type MoveNonStructTypes = MovePrimitive | MoveVector | MoveObject;
export type MoveNonStructTypes = MovePrimitive | MoveVector | MoveObject | MoveOption;

export type MovePrimitive =
| 'bool'
Expand All @@ -18,3 +18,5 @@ export type MovePrimitive =
export type MoveVector = `vector<${string}>`;

export type MoveObject = `0x1::object::Object<${string}>`;

export type MoveOption = `0x1::option::Option<${string}>`;
14 changes: 14 additions & 0 deletions test_contract/sources/test_option.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module surf_addr::test_option {
use std::option::{Option, get_with_default, is_some};

#[view]
public fun test_option_view(t: Option<u64>): u64 {
get_with_default(&t, 0)
}

public entry fun test_option_entry(t: Option<u64>) {
assert!(is_some<u64>(&t), 1);
get_with_default(&t, 0);
}
}

0 comments on commit a0b7dc2

Please sign in to comment.