Skip to content

Commit

Permalink
[sdk] Fix nested type parsing in TypeTag (MystenLabs#7055)
Browse files Browse the repository at this point in the history
I've fixed the parsing of nested types in TypeTag ([this
comment](https://github.com/MystenLabs/sui/blob/main/sdk/typescript/src/signers/txn-data-serializers/type-tag-serializer.ts#L62-L63)).
Also implemented a `tagToString` function which does the oposite of
`parseFromStr` (converts `TagType` -> `string`)

NOTE: I've removed `normalizeSuiAddress` from address field parsing in
`parseFromString` because I don't think this is the right place to use
it (`0x2::sui::SUI` becomes `{ address: 0x000000...00002::sui::SUI,
...}`). Especially because the RPC should support both
MystenLabs#6777:
> For RPC requests, people should be able to pass either way as long as
it's supported by parse_sui_struct_tag.

Changes:
- fixed parsing of nested struct types
- added `parseFromString` function
- made `TypeTagParser` methods static
- exported TypeTagParser in `index.ts`
- removed `normalizeSuiAddress` from parsing struct type address field

Test plan -- added unit tests.
  • Loading branch information
kklas authored Jan 2, 2023
1 parent b8ace6f commit f15daaa
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 26 deletions.
1 change: 1 addition & 0 deletions sdk/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './serialization/hex';
export * from './signers/txn-data-serializers/rpc-txn-data-serializer';
export * from './signers/txn-data-serializers/txn-data-serializer';
export * from './signers/txn-data-serializers/local-txn-data-serializer';
export * from './signers/txn-data-serializers/type-tag-serializer';

export * from './signers/signer';
export * from './signers/raw-signer';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class LocalTxnDataSerializer implements TxnDataSerializer {
function: moveCall.function,
typeArguments: moveCall.typeArguments.map((a) =>
typeof a === 'string'
? new TypeTagSerializer().parseFromStr(a)
? TypeTagSerializer.parseFromStr(a, true)
: (a as TypeTag)
),
arguments: await new CallArgSerializer(
Expand Down
119 changes: 94 additions & 25 deletions sdk/typescript/src/signers/txn-data-serializers/type-tag-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
import { normalizeSuiAddress, TypeTag } from '../../types';

const VECTOR_REGEX = /^vector<(.+)>$/;
const STRUCT_REGEX = /^([^:]+)::([^:]+)::(.+)/;
const STRUCT_TYPE_TAG_REGEX = /^[^<]+<(.+)>$/;
const STRUCT_REGEX = /^([^:]+)::([^:]+)::([^<]+)(<(.+)>)?/;

export class TypeTagSerializer {
parseFromStr(str: string): TypeTag {
static parseFromStr(str: string, normalizeAddress = false): TypeTag {
if (str === 'address') {
return { address: null };
} else if (str === 'bool') {
Expand All @@ -30,38 +29,108 @@ export class TypeTagSerializer {
}
const vectorMatch = str.match(VECTOR_REGEX);
if (vectorMatch) {
return { vector: this.parseFromStr(vectorMatch[1]) };
return {
vector: TypeTagSerializer.parseFromStr(
vectorMatch[1],
normalizeAddress
),
};
}

const structMatch = str.match(STRUCT_REGEX);
if (structMatch) {
try {
return {
struct: {
address: normalizeSuiAddress(structMatch[1]),
module: structMatch[2],
name: structMatch[3].match(/^([^<]+)/)![1],
typeParams: this.parseStructTypeTag(structMatch[3]),
},
};
} catch (e) {
throw new Error(`Encounter error parsing type args for ${str}`);
}
const address = normalizeAddress
? normalizeSuiAddress(structMatch[1])
: structMatch[1];
return {
struct: {
address,
module: structMatch[2],
name: structMatch[3],
typeParams:
structMatch[5] === undefined
? []
: TypeTagSerializer.parseStructTypeArgs(
structMatch[5],
normalizeAddress
),
},
};
}

throw new Error(
`Encounter unexpected token when parsing type args for ${str}`
`Encountered unexpected token when parsing type args for ${str}`
);
}

static parseStructTypeArgs(str: string, normalizeAddress = false): TypeTag[] {
// split `str` by all `,` outside angle brackets
const tok: Array<string> = [];
let word = '';
let nestedAngleBrackets = 0;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === '<') {
nestedAngleBrackets++;
}
if (char === '>') {
nestedAngleBrackets--;
}
if (nestedAngleBrackets === 0 && char === ',') {
tok.push(word.trim());
word = '';
continue;
}
word += char;
}

tok.push(word.trim());

return tok.map((tok) =>
TypeTagSerializer.parseFromStr(tok, normalizeAddress)
);
}

parseStructTypeTag(str: string): TypeTag[] {
const typeTagsMatch = str.match(STRUCT_TYPE_TAG_REGEX);
if (!typeTagsMatch) {
return [];
static tagToString(tag: TypeTag): string {
if ('bool' in tag) {
return 'bool';
}
if ('u8' in tag) {
return 'u8';
}
if ('u16' in tag) {
return 'u16';
}
if ('u32' in tag) {
return 'u32';
}
if ('u64' in tag) {
return 'u64';
}
if ('u128' in tag) {
return 'u128';
}
if ('u256' in tag) {
return 'u256';
}
if ('address' in tag) {
return 'address';
}
if ('signer' in tag) {
return 'signer';
}
if ('vector' in tag) {
return `vector<${TypeTagSerializer.tagToString(tag.vector)}>`;
}
if ('struct' in tag) {
const struct = tag.struct;
const typeParams = struct.typeParams
.map(TypeTagSerializer.tagToString)
.join(', ');
return `${struct.address}::${struct.module}::${struct.name}${
typeParams ? `<${typeParams}>` : ''
}`;
}
// TODO: This will fail if the struct has nested type args with commas. Need
// to implement proper parsing for this case
const typeTags = typeTagsMatch[1].split(',');
return typeTags.map((tag) => this.parseFromStr(tag));
throw new Error('Invalid TypeTag');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { it, describe, expect } from 'vitest';
import { TypeTagSerializer } from '../../../../src/signers/txn-data-serializers/type-tag-serializer';

describe('parseFromStr', () => {
it('parses nested struct type from a string', () => {
const typeStr =
'0x2::balance::Supply<0x72de5feb63c0ab6ed1cda7e5b367f3d0a999add7::amm::LP<0x2::sui::SUI, 0xfee024a3c0c03ada5cdbda7d0e8b68802e6dec80::example_coin::EXAMPLE_COIN>>';
const act = TypeTagSerializer.parseFromStr(typeStr);
const exp = {
struct: {
address: '0x2',
module: 'balance',
name: 'Supply',
typeParams: [
{
struct: {
address: '0x72de5feb63c0ab6ed1cda7e5b367f3d0a999add7',
module: 'amm',
name: 'LP',
typeParams: [
{
struct: {
address: '0x2',
module: 'sui',
name: 'SUI',
typeParams: [],
},
},
{
struct: {
address: '0xfee024a3c0c03ada5cdbda7d0e8b68802e6dec80',
module: 'example_coin',
name: 'EXAMPLE_COIN',
typeParams: [],
},
},
],
},
},
],
},
};
expect(act).toEqual(exp);
});

it('parses non parametrized struct type from a string', () => {
const typeStr = '0x72de5feb63c0ab6ed1cda7e5b367f3d0a999add7::foo::FOO';
const act = TypeTagSerializer.parseFromStr(typeStr);
const exp = {
struct: {
address: '0x72de5feb63c0ab6ed1cda7e5b367f3d0a999add7',
module: 'foo',
name: 'FOO',
typeParams: [],
},
};
expect(act).toEqual(exp);
});
});

describe('tagToString', () => {
it('converts nested struct type to a string', () => {
const type = {
struct: {
address: '0x2',
module: 'balance',
name: 'Supply',
typeParams: [
{
struct: {
address: '0x72de5feb63c0ab6ed1cda7e5b367f3d0a999add7',
module: 'amm',
name: 'LP',
typeParams: [
{
struct: {
address: '0x2',
module: 'sui',
name: 'SUI',
typeParams: [],
},
},
{
struct: {
address: '0xfee024a3c0c03ada5cdbda7d0e8b68802e6dec80',
module: 'example_coin',
name: 'EXAMPLE_COIN',
typeParams: [],
},
},
],
},
},
],
},
};
const act = TypeTagSerializer.tagToString(type);
const exp =
'0x2::balance::Supply<0x72de5feb63c0ab6ed1cda7e5b367f3d0a999add7::amm::LP<0x2::sui::SUI, 0xfee024a3c0c03ada5cdbda7d0e8b68802e6dec80::example_coin::EXAMPLE_COIN>>';
expect(act).toEqual(exp);
});
});

0 comments on commit f15daaa

Please sign in to comment.