Skip to content

Conversation

WGB5445
Copy link
Contributor

@WGB5445 WGB5445 commented Sep 11, 2025

Description

In the current Go SDK, Option uses the BCS-serialized result for deserialization. The benefit of this approach is that 0x00 can represent none, and 0x01 + the serialized bytes of another type can represent some.

However, this behavior is inconsistent with the TS SDK implementation. As a result, parameters that can be serialized correctly in the TS SDK cannot be deserialized properly in Go.

For example:
• Go: supports 0x00 serialization to represent none
• TS: uses null or undefined to represent none
• Go: uses 0x0101 serialization to represent some(1)
• TS: uses "1" to represent some(1)

A partner recently discovered this inconsistency and requested that we align with the TS SDK behavior.

This PR implements that alignment. One important note: when using the TS SDK, passing "" (empty string) as an argument cannot represent none. In Go, only nil can be used to indicate that the variable is none.

Below are the corresponding TS tests.

import { describe, it, expect } from "bun:test";
import {
  parseTypeTag,
  checkOrConvertArgument,
  MoveOption,
} from "@aptos-labs/ts-sdk";

interface TestCase {
  typeTag: string;
  input: any;
  expectedIsSome: boolean;
  expectedBcs: string;
  description: string;
}

const tests: TestCase[] = [
  // Option<u64> tests
  {
    typeTag: "0x1::option::Option<u64>",
    input: "0",
    expectedIsSome: true,
    expectedBcs: "0x010000000000000000", // Some(0) - 0x01 + u64(0) little endian
    description: "treats string '0' as Some(0) for u64",
  },
  {
    typeTag: "0x1::option::Option<u64>",
    input: "123",
    expectedIsSome: true,
    expectedBcs: "0x017b00000000000000", // Some(123) - 0x01 + u64(123) little endian
    description: "treats string '123' as Some(123) for u64",
  },
  {
    typeTag: "0x1::option::Option<u64>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for u64",
  },
  // Option<u8> tests
  {
    typeTag: "0x1::option::Option<u8>",
    input: "0",
    expectedIsSome: true,
    expectedBcs: "0x0100", // Some(0) - 0x01 + u8(0)
    description: "treats string '0' as Some(0) for u8",
  },
  {
    typeTag: "0x1::option::Option<u8>",
    input: "255",
    expectedIsSome: true,
    expectedBcs: "0x01ff", // Some(255) - 0x01 + u8(255)
    description: "treats string '255' as Some(255) for u8",
  },
  {
    typeTag: "0x1::option::Option<u8>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for u8",
  },
  // Option<u32> tests
  {
    typeTag: "0x1::option::Option<u32>",
    input: "0",
    expectedIsSome: true,
    expectedBcs: "0x0100000000", // Some(0) - 0x01 + u32(0) little endian
    description: "treats string '0' as Some(0) for u32",
  },
  {
    typeTag: "0x1::option::Option<u32>",
    input: "100000",
    expectedIsSome: true,
    expectedBcs: "0x01a0860100", // Some(100000) - 0x01 + u32(100000) little endian
    description: "treats string '100000' as Some(100000) for u32",
  },
  {
    typeTag: "0x1::option::Option<u32>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for u32",
  },
  // Option<u128> tests
  {
    typeTag: "0x1::option::Option<u128>",
    input: "0",
    expectedIsSome: true,
    expectedBcs: "0x0100000000000000000000000000000000", // Some(0) - 0x01 + u128(0) little endian
    description: "treats string '0' as Some(0n) for u128",
  },
  {
    typeTag: "0x1::option::Option<u128>",
    input: "123456789",
    expectedIsSome: true,
    expectedBcs: "0x0115cd5b07000000000000000000000000", // Some(123456789) - 0x01 + u128(123456789) little endian
    description: "treats string '123456789' as Some(123456789n) for u128",
  },
  {
    typeTag: "0x1::option::Option<u128>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for u128",
  },
  // Option<u256> tests
  {
    typeTag: "0x1::option::Option<u256>",
    input: "0",
    expectedIsSome: true,
    expectedBcs: "0x010000000000000000000000000000000000000000000000000000000000000000", // Some(0) - 0x01 + u256(0) little endian
    description: "treats string '0' as Some(0n) for u256",
  },
  {
    typeTag: "0x1::option::Option<u256>",
    input: "987654321",
    expectedIsSome: true,
    expectedBcs: "0x01b168de3a00000000000000000000000000000000000000000000000000000000", // Some(987654321) - 0x01 + u256(987654321) little endian
    description: "treats string '987654321' as Some(987654321n) for u256",
  },
  {
    typeTag: "0x1::option::Option<u256>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for u256",
  },
  // Option<address> tests
  {
    typeTag: "0x1::option::Option<address>",
    input: "0x1",
    expectedIsSome: true,
    expectedBcs: "0x010000000000000000000000000000000000000000000000000000000000000001", // Some(0x1) - 0x01 + address(0x1) 32 bytes
    description: "treats address string '0x1' as Some(address)",
  },
  {
    typeTag: "0x1::option::Option<address>",
    input: "0x0",
    expectedIsSome: true,
    expectedBcs: "0x010000000000000000000000000000000000000000000000000000000000000000", // Some(0x0) - 0x01 + address(0x0) 32 bytes
    description: "treats zero address string as Some(address)",
  },
  {
    typeTag: "0x1::option::Option<address>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for address",
  },
  // Option<string> tests
  {
    typeTag: "0x1::option::Option<0x1::string::String>",
    input: "hello",
    expectedIsSome: true,
    expectedBcs: "0x010568656c6c6f", // Some("hello") - 0x01 + ULEB128(5) + "hello" bytes
    description: "treats string 'hello' as Some(String)",
  },
  {
    typeTag: "0x1::option::Option<0x1::string::String>",
    input: "",
    expectedIsSome: true,
    expectedBcs: "0x0100", // Some("") - 0x01 + ULEB128(0)
    description: "treats empty string as Some(String)",
  },
  {
    typeTag: "0x1::option::Option<0x1::string::String>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for String",
  },
  // Option<object> tests
  {
    typeTag: "0x1::option::Option<0x1::object::Object<0x1::aptos_coin::AptosCoin>>",
    input: "0x1",
    expectedIsSome: true,
    expectedBcs: "0x010000000000000000000000000000000000000000000000000000000000000001", // Some(0x1) - 0x01 + address(0x1) 32 bytes
    description: "treats object address '0x1' as Some(Object)",
  },
  {
    typeTag: "0x1::option::Option<0x1::object::Object<0x1::aptos_coin::AptosCoin>>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for Object",
  },
  // Option<vector<u8>> tests
  {
    typeTag: "0x1::option::Option<vector<u8>>",
    input: "0x1234",
    expectedIsSome: true,
    expectedBcs: "0x0106307831323334", // Some(["0x1234" as bytes]) - 0x01 + ULEB128(6) + bytes
    description: "treats hex string '0x1234' as Some(vector<u8>)",
  },
  {
    typeTag: "0x1::option::Option<vector<u8>>",
    input: "",
    expectedIsSome: true,
    expectedBcs: "0x0100", // Some([]) - 0x01 + ULEB128(0)
    description: "treats empty string as Some(empty vector<u8>)",
  },
  {
    typeTag: "0x1::option::Option<vector<u8>>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for vector<u8>",
  },
  // Option<vector<address>> tests
  {
    typeTag: "0x1::option::Option<vector<address>>",
    input: ["0x1", "0x2"],
    expectedIsSome: true,
    expectedBcs: "0x010200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002", // Some([0x1, 0x2]) - 0x01 + ULEB128(2) + address(0x1) + address(0x2)
    description: "treats address array as Some(vector<address>)",
  },
  {
    typeTag: "0x1::option::Option<vector<address>>",
    input: [],
    expectedIsSome: true,
    expectedBcs: "0x0100", // Some([]) - 0x01 + ULEB128(0)
    description: "treats empty array as Some(empty vector<address>)",
  },
  {
    typeTag: "0x1::option::Option<vector<address>>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for vector<address>",
  },
  // Option<vector<string>> tests
  {
    typeTag: "0x1::option::Option<vector<0x1::string::String>>",
    input: ["hello", "world"],
    expectedIsSome: true,
    expectedBcs: "0x01020568656c6c6f05776f726c64", // Some(["hello", "world"]) - 0x01 + ULEB128(2) + string("hello") + string("world")
    description: "treats string array as Some(vector<string>)",
  },
  {
    typeTag: "0x1::option::Option<vector<0x1::string::String>>",
    input: [],
    expectedIsSome: true,
    expectedBcs: "0x0100", // Some([]) - 0x01 + ULEB128(0)
    description: "treats empty string array as Some(empty vector<string>)",
  },
  {
    typeTag: "0x1::option::Option<vector<0x1::string::String>>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for vector<string>",
  },
  // Option<vector<object>> tests
  {
    typeTag: "0x1::option::Option<vector<0x1::object::Object<0x1::aptos_coin::AptosCoin>>>",
    input: ["0x1", "0x2"],
    expectedIsSome: true,
    expectedBcs: "0x010200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002", // Some([0x1, 0x2]) - 0x01 + ULEB128(2) + address(0x1) + address(0x2)
    description: "treats object address array as Some(vector<object>)",
  },
  {
    typeTag: "0x1::option::Option<vector<0x1::object::Object<0x1::aptos_coin::AptosCoin>>>",
    input: [],
    expectedIsSome: true,
    expectedBcs: "0x0100", // Some([]) - 0x01 + ULEB128(0)
    description: "treats empty object array as Some(empty vector<object>)",
  },
  {
    typeTag: "0x1::option::Option<vector<0x1::object::Object<0x1::aptos_coin::AptosCoin>>>",
    input: undefined,
    expectedIsSome: false,
    expectedBcs: "0x00", // None
    description: "treats undefined as None for vector<object>",
  },
];

describe("Option<T> input handling", () => {
  tests.forEach((testCase) => {
    it(testCase.description, () => {
      const typeTag = parseTypeTag(testCase.typeTag);
      const res = checkOrConvertArgument(testCase.input as any, typeTag, 0, []);
      expect(res).toBeInstanceOf(MoveOption);
      const opt = res as MoveOption<any>;
      expect(opt.isSome()).toBe(testCase.expectedIsSome);

      // Verify BCS serialization
      expect(opt.bcsToHex().toString()).toBe(testCase.expectedBcs);
    });
  });
});

… compatibility modes

Added comprehensive tests for Option types including u8, u16, u32, u64, u128, u256, bool, string, and address. Each type includes cases for both standard and compatibility modes, ensuring robust validation of serialization behavior.
Refactored tests for Option types to use direct values instead of hex strings, enhancing clarity and consistency. Adjusted expected outputs for various data types in compatibility mode, ensuring accurate validation of serialization behavior.
- Added support for direct string inputs in ConvertToOption function, enhancing usability for Option types.
- Removed the unused convertCompatibilitySerializedType function to streamline the codebase.
Expanded tests for Option types to include string inputs for u8, u16, u32, u64, u128, u256, bool, and address. Added cases for compatibility mode, ensuring accurate serialization behavior for both direct values and string representations. Improved organization of test cases for better clarity.
@WGB5445 WGB5445 marked this pull request as ready for review September 11, 2025 07:36
@WGB5445 WGB5445 requested review from a team and gregnazario as code owners September 11, 2025 07:36
@WGB5445 WGB5445 requested review from 0x-j and Copilot September 11, 2025 07:36
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Implements Option type compatibility between Go and TypeScript SDKs to ensure consistent serialization behavior. The change aligns Go SDK Option handling with the TS SDK implementation where nil represents None and non-nil values represent Some.

  • Removes complex compatibility mode handling for Option types
  • Simplifies Option serialization to match TS SDK behavior
  • Updates test cases to cover comprehensive Option type scenarios

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
typeConversion.go Simplifies ConvertToOption function by removing compatibility mode handling and unused helper function
typeConversion_test.go Adds comprehensive test cases for Option types with various inner types (u8, u16, u32, u64, u128, u256, bool, address, string, object, vectors)
CHANGELOG.md Documents the feature addition and refactoring changes
Comments suppressed due to low confidence (1)

typeConversion.go:537

  • The function lacks documentation explaining its purpose, parameters, and behavior. Add a comment describing that it converts Go values to Option type serialization compatible with the TS SDK, where nil represents None and non-nil values represent Some.
func ConvertToOption(typeParam TypeTag, arg any, generics []TypeTag, options ...any) ([]byte, error) {
	if arg == nil {
		return bcs.SerializeU8(0)
	}

	b := []byte{1}
	buffer, err := ConvertArg(typeParam, arg, generics, options...)
	if err != nil {
		return nil, err
	}
	return append(b, buffer...), nil
}

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@gregnazario gregnazario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only see deleted code, is it supposed to add some different functionality?

…Option

Added support for compatibility mode in the ConvertToOption function, allowing string representations of various types (u8, u16, u32, u64, u128, u256, bool, address) to be converted into their respective byte formats. Introduced a new helper function, convertCompatibilitySerializedType, to handle serialization for different type tags, enhancing the overall type conversion process.
@WGB5445 WGB5445 changed the title Fix Option compatibility to align with TS SDK (same input, same output) Add Test with TS SDK (same input, same output) Sep 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants