diff --git a/Makefile b/Makefile index b727b87f..1c0fc50e 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,11 @@ test: unit: go test $(TEST_SOURCES_NO_CUCUMBER) - cd test && go test -timeout 0s --godog.strict=true --godog.format=pretty --godog.tags="@unit.offline,@unit.algod,@unit.indexer,@unit.transactions.keyreg,@unit.rekey,@unit.tealsign,@unit.dryrun,@unit.responses,@unit.applications,@unit.transactions,@unit.indexer.rekey,@unit.responses.messagepack,@unit.responses.231,@unit.responses.messagepack.231,@unit.responses.genesis,@unit.feetest,@unit.indexer.logs,@unit.abijson,@unit.abijson.byname,@unit.transactions.payment,@unit.atomic_transaction_composer,@unit.responses.unlimited_assets,@unit.indexer.ledger_refactoring,@unit.algod.ledger_refactoring,@unit.dryrun.trace.application" --test.v . + cd test && go test -timeout 0s --godog.strict=true --godog.format=pretty --godog.tags="@unit.sourcemap,@unit.offline,@unit.algod,@unit.indexer,@unit.transactions.keyreg,@unit.rekey,@unit.tealsign,@unit.dryrun,@unit.responses,@unit.applications,@unit.transactions,@unit.indexer.rekey,@unit.responses.messagepack,@unit.responses.231,@unit.responses.messagepack.231,@unit.responses.genesis,@unit.feetest,@unit.indexer.logs,@unit.abijson,@unit.abijson.byname,@unit.transactions.payment,@unit.atomic_transaction_composer,@unit.responses.unlimited_assets,@unit.indexer.ledger_refactoring,@unit.algod.ledger_refactoring,@unit.dryrun.trace.application" --test.v . integration: go test $(TEST_SOURCES_NO_CUCUMBER) - cd test && go test -timeout 0s --godog.strict=true --godog.format=pretty --godog.tags="@algod,@assets,@auction,@kmd,@send,@indexer,@rekey,@send.keyregtxn,@dryrun,@compile,@applications.verified,@indexer.applications,@indexer.231,@abi,@c2c" --test.v . + cd test && go test -timeout 0s --godog.strict=true --godog.format=pretty --godog.tags="@algod,@assets,@auction,@kmd,@send,@indexer,@rekey,@send.keyregtxn,@dryrun,@compile,@applications.verified,@indexer.applications,@indexer.231,@abi,@c2c,@compile.sourcemap" --test.v . docker-test: ./test/docker/run_docker.sh diff --git a/client/v2/algod/algod.go b/client/v2/algod/algod.go index a43bb93e..80681c63 100644 --- a/client/v2/algod/algod.go +++ b/client/v2/algod/algod.go @@ -29,8 +29,8 @@ func (c *Client) getRaw(ctx context.Context, path string, body interface{}, head // post sends a POST request to the given path with the given request object. // No query parameters will be sent if request is nil. // response must be a pointer to an object as post writes the response there. -func (c *Client) post(ctx context.Context, response interface{}, path string, body interface{}, headers []*common.Header) error { - return (*common.Client)(c).Post(ctx, response, path, body, headers) +func (c *Client) post(ctx context.Context, response interface{}, path string, params interface{}, headers []*common.Header, body interface{}) error { + return (*common.Client)(c).Post(ctx, response, path, params, headers, body) } // MakeClient is the factory for constructing a ClientV2 for a given endpoint. diff --git a/client/v2/algod/dryrun.go b/client/v2/algod/dryrun.go index f6e79b90..89531e26 100644 --- a/client/v2/algod/dryrun.go +++ b/client/v2/algod/dryrun.go @@ -18,7 +18,6 @@ func (s *TealDryRun) Do( ctx context.Context, headers ...*common.Header, ) (response models.DryrunResponse, err error) { - err = s.c.post(ctx, &response, - "/v2/teal/dryrun", s.rawobj, headers) + err = s.c.post(ctx, &response, "/v2/teal/dryrun", nil, headers, s.rawobj) return } diff --git a/client/v2/algod/rawTransaction.go b/client/v2/algod/rawTransaction.go index d97b158a..6815604e 100644 --- a/client/v2/algod/rawTransaction.go +++ b/client/v2/algod/rawTransaction.go @@ -29,7 +29,7 @@ func (s *SendRawTransaction) Do(ctx context.Context, headers ...*common.Header) if addContentType { headers = append(headers, &common.Header{"Content-Type", "application/x-binary"}) } - err = s.c.post(ctx, &response, "/v2/transactions", s.rawtxn, headers) + err = s.c.post(ctx, &response, "/v2/transactions", nil, headers, s.rawtxn) txid = response.Txid return } diff --git a/client/v2/algod/tealCompile.go b/client/v2/algod/tealCompile.go index e3e07bed..18313f24 100644 --- a/client/v2/algod/tealCompile.go +++ b/client/v2/algod/tealCompile.go @@ -36,6 +36,6 @@ func (s *TealCompile) Sourcemap(Sourcemap bool) *TealCompile { // Do performs the HTTP request func (s *TealCompile) Do(ctx context.Context, headers ...*common.Header) (response models.CompileResponse, err error) { - err = s.c.post(ctx, &response, "/v2/teal/compile", s.source, headers) + err = s.c.post(ctx, &response, "/v2/teal/compile", s.p, headers, s.source) return } diff --git a/client/v2/algod/tealDryrun.go b/client/v2/algod/tealDryrun.go index 87e070a4..2299b2cd 100644 --- a/client/v2/algod/tealDryrun.go +++ b/client/v2/algod/tealDryrun.go @@ -19,6 +19,6 @@ type TealDryrun struct { // Do performs the HTTP request func (s *TealDryrun) Do(ctx context.Context, headers ...*common.Header) (response models.DryrunResponse, err error) { - err = s.c.post(ctx, &response, "/v2/teal/dryrun", msgpack.Encode(&s.request), headers) + err = s.c.post(ctx, &response, "/v2/teal/dryrun", nil, headers, msgpack.Encode(&s.request)) return } diff --git a/client/v2/common/common.go b/client/v2/common/common.go index 2314fd6c..f05f3511 100644 --- a/client/v2/common/common.go +++ b/client/v2/common/common.go @@ -103,33 +103,34 @@ func mergeRawQueries(q1, q2 string) string { } // submitFormRaw is a helper used for submitting (ex.) GETs and POSTs to the server -func (client *Client) submitFormRaw(ctx context.Context, path string, body interface{}, requestMethod string, encodeJSON bool, headers []*Header) (resp *http.Response, err error) { +func (client *Client) submitFormRaw(ctx context.Context, path string, params interface{}, requestMethod string, encodeJSON bool, headers []*Header, body interface{}) (resp *http.Response, err error) { queryURL := client.serverURL queryURL.Path += path var req *http.Request var bodyReader io.Reader - if body != nil { - if requestMethod == "POST" && rawRequestPaths[path] { - reqBytes, ok := body.([]byte) - if !ok { - return nil, fmt.Errorf("couldn't decode raw body as bytes") - } - bodyReader = bytes.NewBuffer(reqBytes) - } else { - v, err := query.Values(body) - if err != nil { - return nil, err - } - - queryURL.RawQuery = mergeRawQueries(queryURL.RawQuery, v.Encode()) - if encodeJSON { - jsonValue := json.Encode(body) - bodyReader = bytes.NewBuffer(jsonValue) - } + var v url.Values + + if params != nil { + v, err = query.Values(params) + if err != nil { + return nil, err + } + } + + if requestMethod == "POST" && rawRequestPaths[path] { + reqBytes, ok := body.([]byte) + if !ok { + return nil, fmt.Errorf("couldn't decode raw body as bytes") } + bodyReader = bytes.NewBuffer(reqBytes) + } else if encodeJSON { + jsonValue := json.Encode(params) + bodyReader = bytes.NewBuffer(jsonValue) } + queryURL.RawQuery = mergeRawQueries(queryURL.RawQuery, v.Encode()) + req, err = http.NewRequest(requestMethod, queryURL.String(), bodyReader) if err != nil { return nil, err @@ -161,8 +162,8 @@ func (client *Client) submitFormRaw(ctx context.Context, path string, body inter return resp, nil } -func (client *Client) submitForm(ctx context.Context, response interface{}, path string, body interface{}, requestMethod string, encodeJSON bool, headers []*Header) error { - resp, err := client.submitFormRaw(ctx, path, body, requestMethod, encodeJSON, headers) +func (client *Client) submitForm(ctx context.Context, response interface{}, path string, params interface{}, requestMethod string, encodeJSON bool, headers []*Header, body interface{}) error { + resp, err := client.submitFormRaw(ctx, path, params, requestMethod, encodeJSON, headers, body) if err != nil { return err } @@ -192,14 +193,14 @@ func (client *Client) submitForm(ctx context.Context, response interface{}, path } // Get performs a GET request to the specific path against the server -func (client *Client) Get(ctx context.Context, response interface{}, path string, body interface{}, headers []*Header) error { - return client.submitForm(ctx, response, path, body, "GET", false /* encodeJSON */, headers) +func (client *Client) Get(ctx context.Context, response interface{}, path string, params interface{}, headers []*Header) error { + return client.submitForm(ctx, response, path, params, "GET", false /* encodeJSON */, headers, nil) } // GetRaw performs a GET request to the specific path against the server and returns the raw body bytes. -func (client *Client) GetRaw(ctx context.Context, path string, body interface{}, headers []*Header) (response []byte, err error) { +func (client *Client) GetRaw(ctx context.Context, path string, params interface{}, headers []*Header) (response []byte, err error) { var resp *http.Response - resp, err = client.submitFormRaw(ctx, path, body, "GET", false /* encodeJSON */, headers) + resp, err = client.submitFormRaw(ctx, path, params, "GET", false /* encodeJSON */, headers, nil) if err != nil { return nil, err } @@ -213,8 +214,8 @@ func (client *Client) GetRaw(ctx context.Context, path string, body interface{}, } // GetRawMsgpack performs a GET request to the specific path against the server and returns the decoded messagepack response. -func (client *Client) GetRawMsgpack(ctx context.Context, response interface{}, path string, body interface{}, headers []*Header) error { - resp, err := client.submitFormRaw(ctx, path, body, "GET", false /* encodeJSON */, headers) +func (client *Client) GetRawMsgpack(ctx context.Context, response interface{}, path string, params interface{}, headers []*Header) error { + resp, err := client.submitFormRaw(ctx, path, params, "GET", false /* encodeJSON */, headers, nil) if err != nil { return err } @@ -238,8 +239,8 @@ func (client *Client) GetRawMsgpack(ctx context.Context, response interface{}, p // Post sends a POST request to the given path with the given body object. // No query parameters will be sent if body is nil. // response must be a pointer to an object as post writes the response there. -func (client *Client) Post(ctx context.Context, response interface{}, path string, body interface{}, headers []*Header) error { - return client.submitForm(ctx, response, path, body, "POST", true /* encodeJSON */, headers) +func (client *Client) Post(ctx context.Context, response interface{}, path string, params interface{}, headers []*Header, body interface{}) error { + return client.submitForm(ctx, response, path, params, "POST", true /* encodeJSON */, headers, body) } // Helper function for correctly formatting and escaping URL path parameters. diff --git a/client/v2/indexer/indexer.go b/client/v2/indexer/indexer.go index 9b6e901f..188db972 100644 --- a/client/v2/indexer/indexer.go +++ b/client/v2/indexer/indexer.go @@ -28,8 +28,8 @@ func (c *Client) getRaw(ctx context.Context, path string, body interface{}, head // post sends a POST request to the given path with the given request object. // No query parameters will be sent if request is nil. // response must be a pointer to an object as post writes the response there. -func (c *Client) post(ctx context.Context, response interface{}, path string, body interface{}, headers []*common.Header) error { - return (*common.Client)(c).Post(ctx, response, path, body, headers) +func (c *Client) post(ctx context.Context, response interface{}, path string, params interface{}, headers []*common.Header, body interface{}) error { + return (*common.Client)(c).Post(ctx, response, path, params, headers, body) } // MakeClient is the factory for constructing a ClientV2 for a given endpoint. diff --git a/logic/source_map.go b/logic/source_map.go new file mode 100644 index 00000000..bd6e042f --- /dev/null +++ b/logic/source_map.go @@ -0,0 +1,133 @@ +package logic + +import ( + "fmt" + "strings" +) + +// SourceMap provides a mapping of the source to assembled program +type SourceMap struct { + Version int `json:"version"` + File string `json:"file,omitempty"` + SourceRoot string `json:"sourceRoot,omitempty"` + Sources []string `json:"sources"` + Names []string `json:"names"` + Mappings string `json:"mappings"` + // Decoded mapping results + LineToPc map[int][]int + PcToLine map[int]int +} + +func DecodeSourceMap(ism map[string]interface{}) (SourceMap, error) { + sm := SourceMap{} + + if v, ok := ism["version"]; ok { + sm.Version = int(v.(float64)) + } + + if sm.Version != 3 { + return sm, fmt.Errorf("only version 3 is supported") + } + + if f, ok := ism["file"]; ok { + sm.File = f.(string) + } + + if sr, ok := ism["sourceRoot"]; ok { + sm.SourceRoot = sr.(string) + } + + if srcs, ok := ism["sources"]; ok { + srcSlice := srcs.([]interface{}) + sm.Sources = make([]string, len(srcSlice)) + for idx, s := range srcSlice { + sm.Sources[idx] = s.(string) + } + } + + if names, ok := ism["names"]; ok { + nameSlice := names.([]interface{}) + sm.Names = make([]string, len(nameSlice)) + for idx, n := range nameSlice { + sm.Names[idx] = n.(string) + } + } + + if m, ok := ism["mappings"]; ok { + sm.Mappings = m.(string) + } + + if sm.Mappings == "" { + return sm, fmt.Errorf("no mappings defined") + } + + sm.PcToLine = map[int]int{} + sm.LineToPc = map[int][]int{} + + lastLine := 0 + for idx, chunk := range strings.Split(sm.Mappings, ";") { + vals := decodeSourceMapLine(chunk) + // If the vals length >= 3 the lineDelta + if len(vals) >= 3 { + lastLine = lastLine + vals[2] // Add the line delta + } + + if _, ok := sm.LineToPc[lastLine]; !ok { + sm.LineToPc[lastLine] = []int{} + } + + sm.LineToPc[lastLine] = append(sm.LineToPc[lastLine], idx) + sm.PcToLine[idx] = lastLine + } + + return sm, nil +} + +func (s *SourceMap) GetLineForPc(pc int) (int, bool) { + line, ok := s.PcToLine[pc] + return line, ok +} + +func (s *SourceMap) GetPcsForLine(line int) []int { + return s.LineToPc[line] +} + +const ( + // consts used for vlq encoding/decoding + b64table string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + vlqShiftSize = 5 + vlqFlag = 1 << vlqShiftSize + vlqMask = vlqFlag - 1 +) + +func decodeSourceMapLine(vlq string) []int { + + var ( + results []int + value, shift int + ) + + for i := 0; i < len(vlq); i++ { + digit := strings.Index(b64table, string(vlq[i])) + + value |= (digit & int(vlqMask)) << shift + + if digit&vlqFlag > 0 { + shift += vlqShiftSize + continue + } + + if value&1 > 0 { + value = (value >> 1) * -1 + } else { + value = value >> 1 + } + + results = append(results, value) + + // Reset + value, shift = 0, 0 + } + + return results +} diff --git a/test/algodclientv2_test.go b/test/algodclientv2_test.go index 6873789a..db889d48 100644 --- a/test/algodclientv2_test.go +++ b/test/algodclientv2_test.go @@ -53,6 +53,7 @@ func AlgodClientV2Context(s *godog.Suite) { s.Step(`^we make an Account Information call against account "([^"]*)" with exclude "([^"]*)"$`, weMakeAnAccountInformationCallAgainstAccountWithExclude) s.Step(`^we make an Account Asset Information call against account "([^"]*)" assetID (\d+)$`, weMakeAnAccountAssetInformationCallAgainstAccountAssetID) s.Step(`^we make an Account Application Information call against account "([^"]*)" applicationID (\d+)$`, weMakeAnAccountApplicationInformationCallAgainstAccountApplicationID) + s.BeforeScenario(func(interface{}) { globalErrForExamination = nil }) diff --git a/test/steps_test.go b/test/steps_test.go index 3bc428e6..58581c1f 100644 --- a/test/steps_test.go +++ b/test/steps_test.go @@ -34,6 +34,7 @@ import ( "github.com/algorand/go-algorand-sdk/crypto" "github.com/algorand/go-algorand-sdk/encoding/msgpack" "github.com/algorand/go-algorand-sdk/future" + "github.com/algorand/go-algorand-sdk/logic" "github.com/algorand/go-algorand-sdk/mnemonic" "github.com/algorand/go-algorand-sdk/types" "github.com/cucumber/godog" @@ -107,6 +108,8 @@ var sigTxs [][]byte var accountTxAndSigner future.TransactionWithSigner var txTrace future.DryrunTxnResult var trace string +var sourceMap logic.SourceMap +var srcMapping map[string]interface{} var assetTestFixture struct { Creator string @@ -329,6 +332,12 @@ func FeatureContext(s *godog.Suite) { s.Step(`^I get the method from the Interface by name "([^"]*)"$`, iGetTheMethodFromTheInterfaceByName) s.Step(`^I get the method from the Contract by name "([^"]*)"$`, iGetTheMethodFromTheContractByName) s.Step(`^the produced method signature should equal "([^"]*)"\. If there is an error it begins with "([^"]*)"$`, theProducedMethodSignatureShouldEqualIfThereIsAnErrorItBeginsWith) + s.Step(`^a source map json file "([^"]*)"$`, aSourceMapJsonFile) + s.Step(`^the string composed of pc:line number equals "([^"]*)"$`, theStringComposedOfPclineNumberEquals) + s.Step(`^I compile a teal program "([^"]*)" with mapping enabled$`, iCompileATealProgramWithMappingEnabled) + s.Step(`^the resulting source map is the same as the json "([^"]*)"$`, theResultingSourceMapIsTheSameAsTheJson) + s.Step(`^getting the line associated with a pc "([^"]*)" equals "([^"]*)"$`, gettingTheLineAssociatedWithAPcEquals) + s.Step(`^getting the last pc associated with a line "([^"]*)" equals "([^"]*)"$`, gettingTheLastPcAssociatedWithALineEquals) s.BeforeScenario(func(interface{}) { stxObj = types.SignedTxn{} @@ -2616,3 +2625,113 @@ func callingAppTraceProduces(arg1 string) error { } return nil } + +func aSourceMapJsonFile(srcMapJsonPath string) error { + b, err := loadResource(srcMapJsonPath) + if err != nil { + return err + } + + ism := map[string]interface{}{} + if err := json.Unmarshal(b, &ism); err != nil { + return err + } + + sourceMap, err = logic.DecodeSourceMap(ism) + + return err +} + +func theStringComposedOfPclineNumberEquals(expectedPcToLineString string) error { + var buff []string + for pc := 0; pc < len(sourceMap.PcToLine); pc++ { + line := sourceMap.PcToLine[pc] + buff = append(buff, fmt.Sprintf("%d:%d", pc, line)) + } + actualStr := strings.Join(buff, ";") + if expectedPcToLineString != actualStr { + return fmt.Errorf("Expected %s got %s", expectedPcToLineString, actualStr) + } + return nil +} + +func gettingTheLineAssociatedWithAPcEquals(strPc, strLine string) error { + pc, _ := strconv.Atoi(strPc) + expectedLine, _ := strconv.Atoi(strLine) + + actualLine, ok := sourceMap.GetLineForPc(pc) + if !ok { + return fmt.Errorf("expected valid line, got !ok") + } + + if actualLine != expectedLine { + return fmt.Errorf("expected %d got %d", expectedLine, actualLine) + } + + return nil +} + +func gettingTheLastPcAssociatedWithALineEquals(strLine, strPc string) error { + expectedPc, _ := strconv.Atoi(strPc) + line, _ := strconv.Atoi(strLine) + + pcs := sourceMap.GetPcsForLine(line) + actualPc := pcs[len(pcs)-1] + + if actualPc != expectedPc { + return fmt.Errorf("expected %d got %d", expectedPc, actualPc) + } + + return nil +} + +func iCompileATealProgramWithMappingEnabled(programPath string) error { + fileContents, err := loadResource(programPath) + if err != nil { + return err + } + + result, err := aclv2.TealCompile(fileContents).Sourcemap(true).Do(context.Background()) + if err != nil { + return err + } + + if result.Sourcemap == nil { + return fmt.Errorf("No source map returned") + } + + srcMapping = *result.Sourcemap + return nil +} + +func theResultingSourceMapIsTheSameAsTheJson(expectedJsonPath string) error { + + expectedJson, err := loadResource(expectedJsonPath) + if err != nil { + return err + } + + // Marshal the map to json then unmarshal it so we get alphabetic ordering + expectedMap := map[string]interface{}{} + err = json.Unmarshal(expectedJson, &expectedMap) + if err != nil { + return err + } + + expectedJson, err = json.Marshal(expectedMap) + if err != nil { + return err + } + + // Turn it back into a string + actualJson, err := json.Marshal(srcMapping) + if err != nil { + return nil + } + + if !bytes.Equal(expectedJson, actualJson) { + return fmt.Errorf("expected %s got %s", expectedJson, actualJson) + } + + return nil +}