-module(so_abi_tests). -include_lib("eunit/include/eunit.hrl"). -compile([export_all, nowarn_export_all]). -define(SANDBOX(Code), sandbox(fun() -> Code end)). -define(DUMMY_HASH_WORD, 16#123). -define(DUMMY_HASH_LIT, "#0000000000000000000000000000000000000000000000000000000000000123"). sandbox(Code) -> Parent = self(), Tag = make_ref(), {Pid, Ref} = spawn_monitor(fun() -> Parent ! {Tag, Code()} end), receive {Tag, Res} -> erlang:demonitor(Ref, [flush]), {ok, Res}; {'DOWN', Ref, process, Pid, Reason} -> {error, Reason} after 100 -> exit(Pid, kill), {error, loop} end. from_words(Ws) -> << <<(from_word(W))/binary>> || W <- Ws >>. from_word(W) when is_integer(W) -> <>; from_word(S) when is_list(S) -> Len = length(S), Bin = <<(list_to_binary(S))/binary, 0:(32 - Len)/unit:8>>, <>. encode_decode_test() -> Tests = [42, 1, 0 -1, <<"Hello">>, {tuple, {}}, {tuple, {42}}, {tuple, {21, 37}}, [], [42], [21, 37], {variant, [0, 1], 0, {}}, {variant, [0, 1], 1, {42}}, {variant, [2], 0, {21, 37}}, {typerep, string}, {typerep, integer}, {typerep, {list, integer}}, {typerep, {tuple, [integer]}} ], [?assertEqual(Test, encode_decode(Test)) || Test <- Tests], ok. encode_decode_sophia_test() -> Check = fun(Type, Str) -> case {encode_decode_sophia_string(Type, Str), Str} of {X, X} -> ok; Other -> Other end end, ok = Check("int", "42"), ok = Check("int", "- 42"), ok = Check("bool", "true"), ok = Check("bool", "false"), ok = Check("string", "\"Hello\""), ok = Check("string * list(int) * option(bool)", "(\"Hello\", [1, 2, 3], Some(true))"), ok = Check("variant", "Blue({[\"x\"] = 1})"), ok = Check("r", "{x = (\"foo\", 0), y = Red}"), ok. to_sophia_value_mcl_bls12_381_test() -> Code = "include \"BLS12_381.aes\"\n" "contract C =\n" " entrypoint test_bls12_381_fp(x : int) = BLS12_381.int_to_fp(x)\n" " entrypoint test_bls12_381_fr(x : int) = BLS12_381.int_to_fr(x)\n" " entrypoint test_bls12_381_g1(x : int) = BLS12_381.mk_g1(x, x, x)\n", Opts = [{backend, fate}], CallValue32 = gmb_fate_encoding:serialize({bytes, <<20:256>>}), CallValue48 = gmb_fate_encoding:serialize({bytes, <<55:384>>}), CallValueTp = gmb_fate_encoding:serialize({tuple, {{bytes, <<15:256>>}, {bytes, <<160:256>>}, {bytes, <<1234:256>>}}}), {ok, _} = so_compiler:to_sophia_value(Code, "test_bls12_381_fp", ok, CallValue32, Opts), {error, _} = so_compiler:to_sophia_value(Code, "test_bls12_381_fp", ok, CallValue48, Opts), {ok, _} = so_compiler:to_sophia_value(Code, "test_bls12_381_fr", ok, CallValue48, Opts), {error, _} = so_compiler:to_sophia_value(Code, "test_bls12_381_fr", ok, CallValue32, Opts), {ok, _} = so_compiler:to_sophia_value(Code, "test_bls12_381_g1", ok, CallValueTp, Opts), ok. to_sophia_value_neg_test() -> Code = [ "contract Foo =\n" " entrypoint f(x : int) : string = \"hello\"\n" ], {error, [Err1]} = so_compiler:to_sophia_value(Code, "f", ok, encode(12)), ?assertEqual("Data error:\nCannot translate FATE value 12\n of Sophia type string\n", so_errors:pp(Err1)), {error, [Err2]} = so_compiler:to_sophia_value(Code, "f", revert, encode(12)), ?assertEqual("Data error:\nCould not deserialize the revert message\n", so_errors:pp(Err2)), ok. encode_calldata_neg_test() -> Code = [ "contract Foo =\n" " entrypoint f(x : int) : string = \"hello\"\n" ], ExpErr1 = "Type error at line 5, col 34:\nCannot unify `int` and `bool`\n" "when checking the application of\n" " `f : (int) => string`\n" "to arguments\n" " `true : bool`\n", {error, [Err1]} = so_compiler:create_calldata(Code, "f", ["true"]), ?assertEqual(ExpErr1, so_errors:pp(Err1)), ok. decode_calldata_neg_test() -> Code1 = [ "contract Foo =\n" " entrypoint f(x : int) : string = \"hello\"\n" ], Code2 = [ "contract Foo =\n" " entrypoint f(x : string) : int = 42\n" ], {ok, CallDataFATE} = so_compiler:create_calldata(Code1, "f", ["42"]), {error, [Err1]} = so_compiler:decode_calldata(Code2, "f", <<1,2,3>>), ?assertEqual("Data error:\nFailed to decode calldata binary\n", so_errors:pp(Err1)), {error, [Err2]} = so_compiler:decode_calldata(Code2, "f", CallDataFATE), ?assertEqual("Data error:\nCannot translate FATE value \"*\"\n to Sophia type (string)\n", so_errors:pp(Err2)), {error, [Err3]} = so_compiler:decode_calldata(Code2, "x", CallDataFATE), ?assertEqual("Data error at line 1, col 1:\nFunction 'x' is missing in contract\n", so_errors:pp(Err3)), ok. encode_decode_sophia_string(SophiaType, String) -> io:format("String ~p~n", [String]), Code = [ "contract MakeCall =\n" , " type arg_type = ", SophiaType, "\n" , " type an_alias('a) = string * 'a\n" , " record r = {x : an_alias(int), y : variant}\n" , " datatype variant = Red | Blue(map(string, int))\n" , " entrypoint foo : arg_type => arg_type\n" ], case so_compiler:check_call(lists:flatten(Code), "foo", [String], [no_code]) of {ok, _, [Arg]} -> Data = encode(Arg), case so_compiler:to_sophia_value(Code, "foo", ok, Data, [no_code]) of {ok, Sophia} -> lists:flatten(io_lib:format("~s", [prettypr:format(so_pretty:expr(Sophia))])); {error, Err} -> io:format("~s\n", [Err]), {error, Err} end; {error, Err} -> io:format("~s\n", [Err]), {error, Err} end. calldata_test() -> [42, <<"foobar">>] = encode_decode_calldata("foo", ["int", "string"], ["42", "\"foobar\""]), [{variant, [0,1], 1, {#{ <<"a">> := 4 }}}, {tuple, {{tuple, {<<"b">>, 5}}, {variant, [0,1], 0, {}}}}] = encode_decode_calldata("foo", ["variant", "r"], ["Blue({[\"a\"] = 4})", "{x = (\"b\", 5), y = Red}"]), [{bytes, <<291:256>>}, {address, <<1110:256>>}] = encode_decode_calldata("foo", ["bytes(32)", "address"], [?DUMMY_HASH_LIT, "ak_1111111111111111111111111111113AFEFpt5"]), [{bytes, <<291:256>>}, {bytes, <<291:256>>}] = encode_decode_calldata("foo", ["bytes(32)", "hash"], [?DUMMY_HASH_LIT, ?DUMMY_HASH_LIT]), [119, {bytes, <<0:64/unit:8>>}] = encode_decode_calldata("foo", ["int", "signature"], ["119", [$# | lists:duplicate(128, $0)]]), [{contract, <<1110:256>>}] = encode_decode_calldata("foo", ["Remote"], ["ct_1111111111111111111111111111113AFEFpt5"]), ok. calldata_init_test() -> encode_decode_calldata("init", ["int"], ["42"]), Code = parameterized_contract("foo", ["int"]), encode_decode_calldata_(Code, "init", []), ok. calldata_indent_test() -> Test = fun(Extra) -> Code = parameterized_contract(Extra, "foo", ["int"]), encode_decode_calldata_(Code, "foo", ["42"]) end, Test(" stateful entrypoint bla() = ()"), Test(" type x = int"), Test(" stateful entrypoint bla(x : int) =\n" " x + 1"), Test(" stateful entrypoint bla(x : int) : int =\n" " x + 1"), ok. parameterized_contract(FunName, Types) -> parameterized_contract([], FunName, Types). parameterized_contract(ExtraCode, FunName, Types) -> lists:flatten( ["contract Remote =\n" " entrypoint bla : () => unit\n\n" "main contract Dummy =\n", ExtraCode, "\n", " type an_alias('a) = string * 'a\n" " record r = {x : an_alias(int), y : variant}\n" " datatype variant = Red | Blue(map(string, int))\n" " entrypoint ", FunName, " : (", string:join(Types, ", "), ") => int\n" ]). encode_decode_calldata(FunName, Types, Args) -> Code = parameterized_contract(FunName, Types), encode_decode_calldata_(Code, FunName, Args). encode_decode_calldata_(Code, FunName, Args) -> {ok, Calldata} = so_compiler:create_calldata(Code, FunName, Args, []), {ok, _, _} = so_compiler:check_call(Code, FunName, Args, [no_code]), case FunName of "init" -> []; _ -> {ok, FateArgs} = gmb_fate_abi:decode_calldata(FunName, Calldata), FateArgs end. encode_decode(D) -> ?assertEqual(D, decode(encode(D))), D. encode(D) -> gmb_fate_encoding:serialize(D). decode(B) -> gmb_fate_encoding:deserialize(B).