github.com/algorand/go-algorand-sdk@v1.24.0/test/applications_integration_test.go (about)

     1  package test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha512"
     7  	"encoding/base64"
     8  	"encoding/binary"
     9  	"encoding/json"
    10  	"fmt"
    11  	"reflect"
    12  	"regexp"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/cucumber/godog"
    19  
    20  	"github.com/algorand/go-algorand-sdk/abi"
    21  	"github.com/algorand/go-algorand-sdk/client/v2/algod"
    22  	"github.com/algorand/go-algorand-sdk/client/v2/common/models"
    23  	"github.com/algorand/go-algorand-sdk/client/v2/indexer"
    24  	"github.com/algorand/go-algorand-sdk/crypto"
    25  	sdkJson "github.com/algorand/go-algorand-sdk/encoding/json"
    26  	"github.com/algorand/go-algorand-sdk/future"
    27  	"github.com/algorand/go-algorand-sdk/types"
    28  )
    29  
    30  var algodV2client *algod.Client
    31  var indexerV2client *indexer.Client
    32  var tx types.Transaction
    33  var transientAccount crypto.Account
    34  var applicationId uint64
    35  var applicationIds []uint64
    36  var txComposerResult future.ExecuteResult
    37  
    38  func anAlgodVClientConnectedToPortWithToken(v int, host string, port int, token string) error {
    39  	var err error
    40  	portAsString := strconv.Itoa(port)
    41  	fullHost := "http://" + host + ":" + portAsString
    42  	algodV2client, err = algod.MakeClient(fullHost, token)
    43  	aclv2 = algodV2client
    44  	gh = []byte("MLWBXKMRJ5W3USARAFOHPQJAF4DN6KY3ZJVPIXKODKNN5ZXSZ2DQ")
    45  
    46  	return err
    47  }
    48  
    49  func iCreateANewTransientAccountAndFundItWithMicroalgos(microalgos int) error {
    50  	var err error
    51  
    52  	transientAccount = crypto.GenerateAccount()
    53  
    54  	params, err := algodV2client.SuggestedParams().Do(context.Background())
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	params.Fee = types.MicroAlgos(fee)
    60  	ltxn, err := future.MakePaymentTxn(accounts[1], transientAccount.Address.String(), uint64(microalgos), note, close, params)
    61  	if err != nil {
    62  		return err
    63  	}
    64  	lsk, err := kcl.ExportKey(handle, walletPswd, accounts[1])
    65  	if err != nil {
    66  		return err
    67  	}
    68  	ltxid, lstx, err := crypto.SignTransaction(lsk.PrivateKey, ltxn)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	_, err = algodV2client.SendRawTransaction(lstx).Do(context.Background())
    73  	if err != nil {
    74  		return err
    75  	}
    76  	_, err = future.WaitForConfirmation(algodV2client, ltxid, 1, context.Background())
    77  	if err != nil {
    78  		return err
    79  	}
    80  	return nil
    81  }
    82  
    83  func iFundTheCurrentApplicationsAddress(microalgos int) error {
    84  	address := crypto.GetApplicationAddress(applicationId)
    85  
    86  	params, err := algodV2client.SuggestedParams().Do(context.Background())
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	txn, err := future.MakePaymentTxn(accounts[0], address.String(), uint64(microalgos), nil, "", params)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	res, err := kcl.SignTransaction(handle, walletPswd, txn)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	txid, err = algodV2client.SendRawTransaction(res.SignedTransaction).Do(context.Background())
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	_, err = future.WaitForConfirmation(algodV2client, txid, 1, context.Background())
   107  	return err
   108  }
   109  
   110  func readTealProgram(fileName string) ([]byte, error) {
   111  	fileContents, err := loadResource(fileName)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	if strings.HasSuffix(fileName, ".teal") {
   117  		// need to compile TEAL source file
   118  		response, err := algodV2client.TealCompile(fileContents).Do(context.Background())
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  		return base64.StdEncoding.DecodeString(response.Result)
   123  	}
   124  
   125  	return fileContents, nil
   126  }
   127  
   128  func iBuildAnApplicationTransaction(
   129  	operation, approvalProgram, clearProgram string,
   130  	globalBytes, globalInts, localBytes, localInts int,
   131  	appArgs, foreignApps, foreignAssets, appAccounts string,
   132  	extraPages int, boxes string) error {
   133  
   134  	var clearP []byte
   135  	var approvalP []byte
   136  	var err error
   137  
   138  	var ghbytes [32]byte
   139  	copy(ghbytes[:], gh)
   140  
   141  	var suggestedParams types.SuggestedParams
   142  	suggestedParams, err = algodV2client.SuggestedParams().Do(context.Background())
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	if approvalProgram != "" {
   148  		approvalP, err = readTealProgram(approvalProgram)
   149  		if err != nil {
   150  			return err
   151  		}
   152  	}
   153  
   154  	if clearProgram != "" {
   155  		clearP, err = readTealProgram(clearProgram)
   156  		if err != nil {
   157  			return err
   158  		}
   159  	}
   160  	args, err := parseAppArgs(appArgs)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	var accs []string
   165  	if appAccounts != "" {
   166  		accs = strings.Split(appAccounts, ",")
   167  	}
   168  	fApp, err := splitUint64(foreignApps)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	fAssets, err := splitUint64(foreignAssets)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	staticBoxes, err := parseBoxes(boxes)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	gSchema := types.StateSchema{NumUint: uint64(globalInts), NumByteSlice: uint64(globalBytes)}
   184  	lSchema := types.StateSchema{NumUint: uint64(localInts), NumByteSlice: uint64(localBytes)}
   185  	switch operation {
   186  	case "create":
   187  		tx, err = future.MakeApplicationCreateTxWithBoxes(false, approvalP, clearP,
   188  			gSchema, lSchema, uint32(extraPages), args, accs, fApp, fAssets, staticBoxes,
   189  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   190  	case "create_optin":
   191  		tx, err = future.MakeApplicationCreateTxWithBoxes(true, approvalP, clearP,
   192  			gSchema, lSchema, uint32(extraPages), args, accs, fApp, fAssets, staticBoxes,
   193  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   194  	case "update":
   195  		tx, err = future.MakeApplicationUpdateTxWithBoxes(applicationId, args, accs, fApp, fAssets, staticBoxes,
   196  			approvalP, clearP,
   197  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   198  	case "call":
   199  		tx, err = future.MakeApplicationNoOpTxWithBoxes(applicationId, args, accs,
   200  			fApp, fAssets, staticBoxes,
   201  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   202  	case "optin":
   203  		tx, err = future.MakeApplicationOptInTxWithBoxes(applicationId, args, accs, fApp, fAssets, staticBoxes,
   204  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   205  	case "clear":
   206  		tx, err = future.MakeApplicationClearStateTxWithBoxes(applicationId, args, accs, fApp, fAssets, staticBoxes,
   207  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   208  	case "closeout":
   209  		tx, err = future.MakeApplicationCloseOutTxWithBoxes(applicationId, args, accs, fApp, fAssets, staticBoxes,
   210  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   211  	case "delete":
   212  		tx, err = future.MakeApplicationDeleteTxWithBoxes(applicationId, args, accs, fApp, fAssets, staticBoxes,
   213  			suggestedParams, transientAccount.Address, nil, types.Digest{}, [32]byte{}, types.Address{})
   214  	default:
   215  		return fmt.Errorf("unsupported tx type: %s", operation)
   216  	}
   217  
   218  	return err
   219  }
   220  
   221  func iSignAndSubmitTheTransactionSavingTheTxidIfThereIsAnErrorItIs(expectedErr string) error {
   222  	var err error
   223  	var lstx []byte
   224  
   225  	txid, lstx, err = crypto.SignTransaction(transientAccount.PrivateKey, tx)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	txid, err = algodV2client.SendRawTransaction(lstx).Do(context.Background())
   230  	if err != nil && len(expectedErr) != 0 {
   231  		if strings.Contains(err.Error(), expectedErr) {
   232  			return nil
   233  		}
   234  	}
   235  	return err
   236  }
   237  
   238  func iWaitForTheTransactionToBeConfirmed() error {
   239  	_, err := future.WaitForConfirmation(algodV2client, txid, 1, context.Background())
   240  	if err != nil {
   241  		return err
   242  	}
   243  	return nil
   244  }
   245  
   246  func iRememberTheNewApplicationID() error {
   247  	response, _, err := algodV2client.PendingTransactionInformation(txid).Do(context.Background())
   248  	applicationId = response.ApplicationIndex
   249  	applicationIds = append(applicationIds, applicationId)
   250  	return err
   251  }
   252  
   253  func iResetTheArrayOfApplicationIDsToRemember() error {
   254  	applicationIds = nil
   255  	return nil
   256  }
   257  
   258  func iGetTheAccountAddressForTheCurrentApp() error {
   259  	actual := crypto.GetApplicationAddress(applicationId)
   260  
   261  	prefix := []byte("appID")
   262  	preimage := make([]byte, len(prefix)+8) // 8 = number of bytes in a uint64
   263  	copy(preimage, prefix)
   264  	binary.BigEndian.PutUint64(preimage[len(prefix):], applicationId)
   265  
   266  	expected := types.Address(sha512.Sum512_256(preimage))
   267  
   268  	if expected != actual {
   269  		return fmt.Errorf("Addresses do not match. Expected %s, got %s", expected.String(), actual.String())
   270  	}
   271  	return nil
   272  }
   273  
   274  func parseAppArgs(appArgsString string) (appArgs [][]byte, err error) {
   275  	if appArgsString == "" {
   276  		return make([][]byte, 0), nil
   277  	}
   278  	argsArray := strings.Split(appArgsString, ",")
   279  	resp := make([][]byte, len(argsArray))
   280  
   281  	for idx, arg := range argsArray {
   282  		typeArg := strings.Split(arg, ":")
   283  		switch typeArg[0] {
   284  		case "str":
   285  			resp[idx] = []byte(typeArg[1])
   286  		case "int":
   287  			intval, _ := strconv.ParseUint(typeArg[1], 10, 64)
   288  
   289  			buf := new(bytes.Buffer)
   290  			err := binary.Write(buf, binary.BigEndian, intval)
   291  			if err != nil {
   292  				return nil, fmt.Errorf("failed to convert %s to bytes", arg)
   293  			}
   294  			resp[idx] = buf.Bytes()
   295  		case "b64":
   296  			d, err := (base64.StdEncoding.DecodeString(typeArg[1]))
   297  			if err != nil {
   298  				return nil, fmt.Errorf("failed to b64 decode arg = %s", arg)
   299  			}
   300  			resp[idx] = d
   301  		default:
   302  			return nil, fmt.Errorf("Applications doesn't currently support argument of type %s", typeArg[0])
   303  		}
   304  	}
   305  	return resp, err
   306  }
   307  
   308  func parseBoxes(boxesStr string) (staticBoxes []types.AppBoxReference, err error) {
   309  	if boxesStr == "" {
   310  		return make([]types.AppBoxReference, 0), nil
   311  	}
   312  
   313  	boxesArray := strings.Split(boxesStr, ",")
   314  
   315  	for i := 0; i < len(boxesArray); i += 2 {
   316  		appID, err := strconv.ParseUint(boxesArray[i], 10, 64)
   317  		if err != nil {
   318  			return nil, err
   319  		}
   320  
   321  		var nameBytes []byte
   322  		nameArray := strings.Split(boxesArray[i+1], ":")
   323  		if nameArray[0] == "str" {
   324  			nameBytes = []byte(nameArray[1])
   325  		} else {
   326  			nameBytes, err = base64.StdEncoding.DecodeString(nameArray[1])
   327  			if err != nil {
   328  				return nil, err
   329  			}
   330  		}
   331  
   332  		if len(nameBytes) == 0 {
   333  			nameBytes = nil
   334  		}
   335  
   336  		staticBoxes = append(staticBoxes,
   337  			types.AppBoxReference{
   338  				AppID: appID,
   339  				Name:  nameBytes,
   340  			})
   341  	}
   342  
   343  	return
   344  }
   345  
   346  func splitUint64(uint64s string) ([]uint64, error) {
   347  	if uint64s == "" {
   348  		return make([]uint64, 0), nil
   349  	}
   350  	uintarr := strings.Split(uint64s, ",")
   351  	resp := make([]uint64, len(uintarr))
   352  	var err error
   353  	for idx, val := range uintarr {
   354  		resp[idx], err = strconv.ParseUint(val, 10, 64)
   355  		if err != nil {
   356  			return nil, err
   357  		}
   358  	}
   359  	return resp, nil
   360  }
   361  
   362  func theTransientAccountShouldHave(appCreated string, byteSlices, uints int,
   363  	applicationState, key, value string) error {
   364  
   365  	acct, err := algodV2client.AccountInformation(transientAccount.Address.String()).Do(context.Background())
   366  	if err != nil {
   367  		return err
   368  	}
   369  	if acct.AppsTotalSchema.NumByteSlice != uint64(byteSlices) {
   370  		return fmt.Errorf("expected NumByteSlices %d, got %d",
   371  			byteSlices, acct.AppsTotalSchema.NumByteSlice)
   372  	}
   373  	if acct.AppsTotalSchema.NumUint != uint64(uints) {
   374  		return fmt.Errorf("expected NumUnit %d, got %d",
   375  			uints, acct.AppsTotalSchema.NumUint)
   376  	}
   377  	created, err := strconv.ParseBool(appCreated)
   378  	if err != nil {
   379  		return err
   380  	}
   381  
   382  	// If we don't expect the app to exist, verify that it isn't there and exit.
   383  	if !created {
   384  		for _, ap := range acct.CreatedApps {
   385  			if ap.Id == applicationId {
   386  				return fmt.Errorf("AppId is not expected to be in the account")
   387  			}
   388  		}
   389  		return nil
   390  	}
   391  
   392  	appIdFound := false
   393  	for _, ap := range acct.CreatedApps {
   394  		if ap.Id == applicationId {
   395  			appIdFound = true
   396  			break
   397  		}
   398  	}
   399  	if !appIdFound {
   400  		return fmt.Errorf("AppId %d is not found in the account", applicationId)
   401  	}
   402  
   403  	// If there is no key to check, we're done.
   404  	if key == "" {
   405  		return nil
   406  	}
   407  
   408  	// Verify the key-value is set
   409  	found := false
   410  	var keyValues []models.TealKeyValue
   411  
   412  	switch strings.ToLower(applicationState) {
   413  	case "local":
   414  		count := 0
   415  		for _, als := range acct.AppsLocalState {
   416  			if als.Id == applicationId {
   417  				keyValues = als.KeyValue
   418  				count++
   419  			}
   420  		}
   421  		if count != 1 {
   422  			return fmt.Errorf("Expected only one matching AppsLocalState, found %d",
   423  				count)
   424  		}
   425  	case "global":
   426  		count := 0
   427  		for _, als := range acct.CreatedApps {
   428  			if als.Id == applicationId {
   429  				keyValues = als.Params.GlobalState
   430  				count++
   431  			}
   432  		}
   433  		if count != 1 {
   434  			return fmt.Errorf("Expected only one matching CreatedApps, found %d",
   435  				count)
   436  		}
   437  	default:
   438  		return fmt.Errorf("Unknown application state: %s", applicationState)
   439  	}
   440  
   441  	if len(keyValues) == 0 {
   442  		return fmt.Errorf("Expected keyvalues length to be greater than 0")
   443  	}
   444  	for _, kv := range keyValues {
   445  		if kv.Key == key {
   446  			if kv.Value.Type == 1 {
   447  				if kv.Value.Bytes != value {
   448  					return fmt.Errorf("Value mismatch: expected: '%s', got '%s'",
   449  						value, kv.Value.Bytes)
   450  				}
   451  			} else if kv.Value.Type == 0 {
   452  				val, err := strconv.ParseUint(value, 10, 64)
   453  				if err != nil {
   454  					return err
   455  				}
   456  				if kv.Value.Uint != val {
   457  					return fmt.Errorf("Value mismatch: expected: '%s', got '%s'",
   458  						value, kv.Value.Bytes)
   459  				}
   460  			}
   461  			found = true
   462  		}
   463  	}
   464  	if !found {
   465  		return fmt.Errorf("Could not find key '%s'", key)
   466  	}
   467  	return nil
   468  }
   469  
   470  func suggestedParamsAlgodV2() error {
   471  	var err error
   472  	sugParams, err = aclv2.SuggestedParams().Do(context.Background())
   473  	return err
   474  }
   475  
   476  func iAddTheCurrentTransactionWithSignerToTheComposer() error {
   477  	err := txComposer.AddTransaction(accountTxAndSigner)
   478  	return err
   479  }
   480  
   481  func iCloneTheComposer() error {
   482  	txComposer = txComposer.Clone()
   483  	return nil
   484  }
   485  
   486  func iExecuteTheCurrentTransactionGroupWithTheComposer() error {
   487  	var err error
   488  	txComposerResult, err = txComposer.Execute(algodV2client, context.Background(), 10)
   489  	return err
   490  }
   491  
   492  func theAppShouldHaveReturned(commaSeparatedB64Results string) error {
   493  	b64ExpectedResults := strings.Split(commaSeparatedB64Results, ",")
   494  
   495  	if len(b64ExpectedResults) != len(txComposerResult.MethodResults) {
   496  		return fmt.Errorf("length of expected results doesn't match actual: %d != %d", len(b64ExpectedResults), len(txComposerResult.MethodResults))
   497  	}
   498  
   499  	for i, b64ExpectedResult := range b64ExpectedResults {
   500  		expectedResult, err := base64.StdEncoding.DecodeString(b64ExpectedResult)
   501  		if err != nil {
   502  			return err
   503  		}
   504  
   505  		actualResult := txComposerResult.MethodResults[i]
   506  		method := actualResult.Method
   507  
   508  		if actualResult.DecodeError != nil {
   509  			return actualResult.DecodeError
   510  		}
   511  
   512  		if !bytes.Equal(actualResult.RawReturnValue, expectedResult) {
   513  			return fmt.Errorf("Actual result does not match expected result. Actual: %s\n", base64.StdEncoding.EncodeToString(actualResult.RawReturnValue))
   514  		}
   515  
   516  		if method.Returns.IsVoid() {
   517  			if len(expectedResult) > 0 {
   518  				return fmt.Errorf("Expected result should be empty")
   519  			}
   520  			continue
   521  		}
   522  
   523  		abiReturnType, err := method.Returns.GetTypeObject()
   524  		if err != nil {
   525  			return err
   526  		}
   527  
   528  		expectedValue, err := abiReturnType.Decode(expectedResult)
   529  		if err != nil {
   530  			return err
   531  		}
   532  
   533  		if !reflect.DeepEqual(expectedValue, actualResult.ReturnValue) {
   534  			return fmt.Errorf("the decoded expected value doesn't match the actual result")
   535  		}
   536  	}
   537  
   538  	return nil
   539  }
   540  
   541  func theAppShouldHaveReturnedABITypes(colonSeparatedExpectedTypeStrings string) error {
   542  	expectedTypeStrings := strings.Split(colonSeparatedExpectedTypeStrings, ":")
   543  
   544  	if len(expectedTypeStrings) != len(txComposerResult.MethodResults) {
   545  		return fmt.Errorf("length of expected results doesn't match actual: %d != %d", len(expectedTypeStrings), len(txComposerResult.MethodResults))
   546  	}
   547  
   548  	for i, expectedTypeString := range expectedTypeStrings {
   549  		actualResult := txComposerResult.MethodResults[i]
   550  
   551  		if actualResult.DecodeError != nil {
   552  			return actualResult.DecodeError
   553  		}
   554  
   555  		if expectedTypeString == abi.VoidReturnType {
   556  			if len(actualResult.RawReturnValue) != 0 {
   557  				return fmt.Errorf("No return bytes were expected, but some are present")
   558  			}
   559  			continue
   560  		}
   561  
   562  		expectedType, err := abi.TypeOf(expectedTypeString)
   563  		if err != nil {
   564  			return err
   565  		}
   566  
   567  		decoded, err := expectedType.Decode(actualResult.RawReturnValue)
   568  		if err != nil {
   569  			return err
   570  		}
   571  		reencoded, err := expectedType.Encode(decoded)
   572  		if err != nil {
   573  			return err
   574  		}
   575  
   576  		if !bytes.Equal(reencoded, actualResult.RawReturnValue) {
   577  			return fmt.Errorf("The round trip result does not match the original result")
   578  		}
   579  	}
   580  
   581  	return nil
   582  }
   583  
   584  func checkAtomicResultAgainstValue(resultIndex int, path, expectedValue string) error {
   585  	keys := strings.Split(path, ".")
   586  	info := txComposerResult.MethodResults[resultIndex].TransactionInfo
   587  
   588  	jsonBytes := sdkJson.Encode(&info)
   589  
   590  	var genericJson interface{}
   591  	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
   592  	decoder.UseNumber()
   593  	err := decoder.Decode(&genericJson)
   594  	if err != nil {
   595  		return err
   596  	}
   597  
   598  	for i, key := range keys {
   599  		var value interface{}
   600  
   601  		intKey, err := strconv.Atoi(key)
   602  		if err == nil {
   603  			// key is an array index
   604  			genericArray, ok := genericJson.([]interface{})
   605  			if !ok {
   606  				return fmt.Errorf("Path component %d is an array index (%d), but the parent is not an array. Parent type: %s", i, intKey, reflect.TypeOf(genericJson))
   607  			}
   608  			if intKey < 0 || intKey >= len(genericArray) {
   609  				return fmt.Errorf("Path component %d is an array index (%d) outside of the parent array's range. Parent length: %d", i, intKey, len(genericArray))
   610  			}
   611  			value = genericArray[intKey]
   612  		} else {
   613  			// key is an object field
   614  			genericObject, ok := genericJson.(map[string]interface{})
   615  			if !ok {
   616  				return fmt.Errorf("Path component %d is an object field ('%s'), but the parent is not an object. Parent type: %s", i, key, reflect.TypeOf(genericJson))
   617  			}
   618  			value, ok = genericObject[key]
   619  			if !ok {
   620  				var parentFields []string
   621  				for field := range genericObject {
   622  					parentFields = append(parentFields, "'"+field+"'")
   623  				}
   624  				return fmt.Errorf("Path component %d is an object field ('%s'), but the parent does not contain the field. Parent fields are: %s", i, key, strings.Join(parentFields, ","))
   625  			}
   626  		}
   627  
   628  		genericJson = value
   629  	}
   630  
   631  	// we have arrived at the target object
   632  	switch actual := genericJson.(type) {
   633  	case string:
   634  		if actual != expectedValue {
   635  			return fmt.Errorf("String values not equal. Expected '%s', got '%s'", expectedValue, actual)
   636  		}
   637  	case json.Number:
   638  		actualParsed, err := strconv.ParseUint(actual.String(), 10, 64)
   639  		if err != nil {
   640  			return err
   641  		}
   642  		expectedParsed, err := strconv.ParseUint(expectedValue, 10, 64)
   643  		if err != nil {
   644  			return err
   645  		}
   646  		if actualParsed != expectedParsed {
   647  			return fmt.Errorf("Int values not equal. Expected %d, got %d", expectedParsed, actualParsed)
   648  		}
   649  	default:
   650  		return fmt.Errorf("The final path element does not resolve to a support type. Type is %s", reflect.TypeOf(genericJson))
   651  	}
   652  
   653  	return nil
   654  }
   655  
   656  func checkInnerTxnGroupIDs(colonSeparatedPathsString string) error {
   657  	paths := [][]int{}
   658  
   659  	commaSeparatedPathStrings := strings.Split(colonSeparatedPathsString, ":")
   660  	for _, commaSeparatedPathString := range commaSeparatedPathStrings {
   661  		pathOfStrings := strings.Split(commaSeparatedPathString, ",")
   662  		path := make([]int, len(pathOfStrings))
   663  		for i, stringComponent := range pathOfStrings {
   664  			intComponent, err := strconv.Atoi(stringComponent)
   665  			if err != nil {
   666  				return err
   667  			}
   668  			path[i] = intComponent
   669  		}
   670  		paths = append(paths, path)
   671  	}
   672  
   673  	var txInfosToCheck []models.PendingTransactionResponse
   674  
   675  	for _, path := range paths {
   676  		var current models.PendingTransactionResponse
   677  		for pathIndex, innerTxnIndex := range path {
   678  			if pathIndex == 0 {
   679  				current = models.PendingTransactionResponse(txComposerResult.MethodResults[innerTxnIndex].TransactionInfo)
   680  			} else {
   681  				current = current.InnerTxns[innerTxnIndex]
   682  			}
   683  		}
   684  		txInfosToCheck = append(txInfosToCheck, current)
   685  	}
   686  
   687  	var group types.Digest
   688  	for i, txInfo := range txInfosToCheck {
   689  		if i == 0 {
   690  			group = txInfo.Transaction.Txn.Group
   691  		}
   692  
   693  		if group != txInfo.Transaction.Txn.Group {
   694  			return fmt.Errorf("Group hashes differ: %s != %s", group, txInfo.Transaction.Txn.Group)
   695  		}
   696  	}
   697  
   698  	return nil
   699  }
   700  
   701  func checkSpinResult(resultIndex int, method, r string) error {
   702  	if method != "spin()" {
   703  		return fmt.Errorf("Incorrect method name, expected 'spin()', got '%s'", method)
   704  	}
   705  
   706  	result := txComposerResult.MethodResults[resultIndex]
   707  	decodedResult := result.ReturnValue.([]interface{})
   708  
   709  	spin := decodedResult[0].([]interface{})
   710  	spinBytes := make([]byte, len(spin))
   711  	for i, value := range spin {
   712  		spinBytes[i] = value.(byte)
   713  	}
   714  
   715  	matched, err := regexp.Match(r, spinBytes)
   716  	if err != nil {
   717  		return err
   718  	}
   719  
   720  	if !matched {
   721  		return fmt.Errorf("Result did not match regex. Spin result: %s", string(spinBytes))
   722  	}
   723  
   724  	return nil
   725  }
   726  
   727  func sha512_256AsUint64(preimage []byte) uint64 {
   728  	hashed := sha512.Sum512_256(preimage)
   729  	return binary.BigEndian.Uint64(hashed[:8])
   730  }
   731  
   732  func checkRandomIntResult(resultIndex, input int) error {
   733  	result := txComposerResult.MethodResults[resultIndex]
   734  	decodedResult := result.ReturnValue.([]interface{})
   735  
   736  	randInt := decodedResult[0].(uint64)
   737  
   738  	witness := decodedResult[1].([]interface{})
   739  	witnessBytes := make([]byte, len(witness))
   740  	for i, value := range witness {
   741  		witnessBytes[i] = value.(byte)
   742  	}
   743  
   744  	x := sha512_256AsUint64(witnessBytes)
   745  	quotient := x % uint64(input)
   746  	if quotient != randInt {
   747  		return fmt.Errorf("Unexpected result: quotient is %d and randInt is %d", quotient, randInt)
   748  	}
   749  
   750  	return nil
   751  }
   752  
   753  func checkRandomElementResult(resultIndex int, input string) error {
   754  	result := txComposerResult.MethodResults[resultIndex]
   755  	decodedResult := result.ReturnValue.([]interface{})
   756  
   757  	randElt := decodedResult[0].(byte)
   758  
   759  	witness := decodedResult[1].([]interface{})
   760  	witnessBytes := make([]byte, len(witness))
   761  	for i, value := range witness {
   762  		witnessBytes[i] = value.(byte)
   763  	}
   764  
   765  	x := sha512_256AsUint64(witnessBytes)
   766  	quotient := x % uint64(len(input))
   767  	if input[quotient] != randElt {
   768  		return fmt.Errorf("Unexpected result: input[quotient] is %d and randElt is %d", input[quotient], randElt)
   769  	}
   770  
   771  	return nil
   772  }
   773  
   774  func theContentsOfTheBoxWithNameShouldBeIfThereIsAnErrorItIs(fromClient, encodedBoxName, boxContents, errStr string) error {
   775  	var box models.Box
   776  	decodedBoxNames, err := parseAppArgs(encodedBoxName)
   777  	if err != nil {
   778  		return err
   779  	}
   780  	decodedBoxName := decodedBoxNames[0]
   781  	if fromClient == "algod" {
   782  		box, err = algodV2client.GetApplicationBoxByName(applicationId, decodedBoxName).Do(context.Background())
   783  	} else if fromClient == "indexer" {
   784  		box, err = indexerV2client.LookupApplicationBoxByIDAndName(applicationId, decodedBoxName).Do(context.Background())
   785  	} else {
   786  		err = fmt.Errorf("expecting algod or indexer, got " + fromClient)
   787  	}
   788  	if err != nil {
   789  		if strings.Contains(err.Error(), errStr) {
   790  			return nil
   791  		}
   792  		return err
   793  	}
   794  
   795  	b64Value := base64.StdEncoding.EncodeToString(box.Value)
   796  	if b64Value != boxContents {
   797  		return fmt.Errorf("expected box value %s is not equal to actual box value %s", boxContents, b64Value)
   798  	}
   799  
   800  	return nil
   801  }
   802  
   803  func currentApplicationShouldHaveFollowingBoxes(fromClient, encodedBoxesRaw string) error {
   804  	var expectedNames [][]byte
   805  	if len(encodedBoxesRaw) > 0 {
   806  		encodedBoxes := strings.Split(encodedBoxesRaw, ":")
   807  		expectedNames = make([][]byte, len(encodedBoxes))
   808  		for i, b := range encodedBoxes {
   809  			expected, err := base64.StdEncoding.DecodeString(b)
   810  			if err != nil {
   811  				return err
   812  			}
   813  			expectedNames[i] = expected
   814  		}
   815  	}
   816  	sort.Slice(expectedNames, func(i, j int) bool {
   817  		return bytes.Compare(expectedNames[i], expectedNames[j]) < 0
   818  	})
   819  
   820  	var r models.BoxesResponse
   821  	var err error
   822  
   823  	if fromClient == "algod" {
   824  		r, err = algodV2client.GetApplicationBoxes(applicationId).Do(context.Background())
   825  	} else if fromClient == "indexer" {
   826  		r, err = indexerV2client.SearchForApplicationBoxes(applicationId).Do(context.Background())
   827  	} else {
   828  		err = fmt.Errorf("expecting algod or indexer, got " + fromClient)
   829  	}
   830  	if err != nil {
   831  		return err
   832  	}
   833  
   834  	actualNames := make([][]byte, len(r.Boxes))
   835  	for i, b := range r.Boxes {
   836  		actualNames[i] = b.Name
   837  	}
   838  	sort.Slice(actualNames, func(i, j int) bool {
   839  		return bytes.Compare(actualNames[i], actualNames[j]) < 0
   840  	})
   841  
   842  	err = sliceOfBytesEqual(expectedNames, actualNames)
   843  	if err != nil {
   844  		return fmt.Errorf("expected and actual box names do not match: %w", err)
   845  	}
   846  
   847  	return nil
   848  }
   849  
   850  func sleptForNMilliSecsForIndexer(n int) error {
   851  	time.Sleep(time.Duration(n) * time.Millisecond)
   852  	return nil
   853  }
   854  
   855  func currentApplicationShouldHaveBoxNum(fromClient string, max int, expectedNum int) error {
   856  	var r models.BoxesResponse
   857  	var err error
   858  
   859  	if fromClient == "algod" {
   860  		r, err = algodV2client.GetApplicationBoxes(applicationId).Max(uint64(max)).Do(context.Background())
   861  	} else if fromClient == "indexer" {
   862  		r, err = indexerV2client.SearchForApplicationBoxes(applicationId).Limit(uint64(max)).Do(context.Background())
   863  	} else {
   864  		err = fmt.Errorf("expecting algod or indexer, got " + fromClient)
   865  	}
   866  	if err != nil {
   867  		return err
   868  	}
   869  
   870  	if len(r.Boxes) != expectedNum {
   871  		return fmt.Errorf("expected and actual box number do not match: %v != %v", expectedNum, len(r.Boxes))
   872  	}
   873  	return nil
   874  }
   875  
   876  func indexerSaysCurrentAppShouldHaveTheseBoxes(max int, next string, encodedBoxesRaw string) error {
   877  	r, err := indexerV2client.SearchForApplicationBoxes(applicationId).Limit(uint64(max)).Next(next).Do(context.Background())
   878  	if err != nil {
   879  		return err
   880  	}
   881  	actualNames := make([][]byte, len(r.Boxes))
   882  	for i, b := range r.Boxes {
   883  		actualNames[i] = b.Name
   884  	}
   885  	sort.Slice(actualNames, func(i, j int) bool {
   886  		return bytes.Compare(actualNames[i], actualNames[j]) < 0
   887  	})
   888  
   889  	var expectedNames [][]byte
   890  	if len(encodedBoxesRaw) > 0 {
   891  		encodedBoxes := strings.Split(encodedBoxesRaw, ":")
   892  		expectedNames = make([][]byte, len(encodedBoxes))
   893  		for i, b := range encodedBoxes {
   894  			expected, err := base64.StdEncoding.DecodeString(b)
   895  			if err != nil {
   896  				return err
   897  			}
   898  			expectedNames[i] = expected
   899  		}
   900  	}
   901  	sort.Slice(expectedNames, func(i, j int) bool {
   902  		return bytes.Compare(expectedNames[i], expectedNames[j]) < 0
   903  	})
   904  
   905  	err = sliceOfBytesEqual(expectedNames, actualNames)
   906  	if err != nil {
   907  		return fmt.Errorf("expected and actual box names do not match: %w", err)
   908  	}
   909  
   910  	return nil
   911  }
   912  
   913  // @applications.verified and @applications.boxes
   914  func ApplicationsContext(s *godog.Suite) {
   915  	s.Step(`^an algod v(\d+) client connected to "([^"]*)" port (\d+) with token "([^"]*)"$`, anAlgodVClientConnectedToPortWithToken)
   916  	s.Step(`^I create a new transient account and fund it with (\d+) microalgos\.$`, iCreateANewTransientAccountAndFundItWithMicroalgos)
   917  	s.Step(`^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+), boxes "([^"]*)"$`, iBuildAnApplicationTransaction)
   918  	s.Step(`^I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$`, iSignAndSubmitTheTransactionSavingTheTxidIfThereIsAnErrorItIs)
   919  	s.Step(`^I wait for the transaction to be confirmed\.$`, iWaitForTheTransactionToBeConfirmed)
   920  	s.Step(`^I remember the new application ID\.$`, iRememberTheNewApplicationID)
   921  	s.Step(`^I reset the array of application IDs to remember\.$`, iResetTheArrayOfApplicationIDsToRemember)
   922  	s.Step(`^I get the account address for the current application and see that it matches the app id\'s hash$`, iGetTheAccountAddressForTheCurrentApp)
   923  	s.Step(`^The transient account should have the created app "([^"]*)" and total schema byte-slices (\d+) and uints (\d+), the application "([^"]*)" state contains key "([^"]*)" with value "([^"]*)"$`,
   924  		theTransientAccountShouldHave)
   925  	s.Step(`^suggested transaction parameters from the algod v2 client$`, suggestedParamsAlgodV2)
   926  	s.Step(`^I add the current transaction with signer to the composer\.$`, iAddTheCurrentTransactionWithSignerToTheComposer)
   927  	s.Step(`^I clone the composer\.$`, iCloneTheComposer)
   928  	s.Step(`^I execute the current transaction group with the composer\.$`, iExecuteTheCurrentTransactionGroupWithTheComposer)
   929  	s.Step(`^The app should have returned "([^"]*)"\.$`, theAppShouldHaveReturned)
   930  	s.Step(`^The app should have returned ABI types "([^"]*)"\.$`, theAppShouldHaveReturnedABITypes)
   931  
   932  	s.Step(`^I fund the current application\'s address with (\d+) microalgos\.$`, iFundTheCurrentApplicationsAddress)
   933  
   934  	s.Step(`^I can dig the (\d+)th atomic result with path "([^"]*)" and see the value "([^"]*)"$`, checkAtomicResultAgainstValue)
   935  	s.Step(`^I dig into the paths "([^"]*)" of the resulting atomic transaction tree I see group ids and they are all the same$`, checkInnerTxnGroupIDs)
   936  	s.Step(`^The (\d+)th atomic result for "([^"]*)" satisfies the regex "([^"]*)"$`, checkSpinResult)
   937  
   938  	s.Step(`^The (\d+)th atomic result for randomInt\((\d+)\) proves correct$`, checkRandomIntResult)
   939  	s.Step(`^The (\d+)th atomic result for randElement\("([^"]*)"\) proves correct$`, checkRandomElementResult)
   940  
   941  	s.Step(`^according to "([^"]*)", the contents of the box with name "([^"]*)" in the current application should be "([^"]*)"\. If there is an error it is "([^"]*)"\.$`, theContentsOfTheBoxWithNameShouldBeIfThereIsAnErrorItIs)
   942  	s.Step(`^according to "([^"]*)", the current application should have the following boxes "([^"]*)"\.$`, currentApplicationShouldHaveFollowingBoxes)
   943  	s.Step(`^according to "([^"]*)", with (\d+) being the parameter that limits results, the current application should have (\d+) boxes\.$`, currentApplicationShouldHaveBoxNum)
   944  	s.Step(`^according to indexer, with (\d+) being the parameter that limits results, and "([^"]*)" being the parameter that sets the next result, the current application should have the following boxes "([^"]*)"\.$`, indexerSaysCurrentAppShouldHaveTheseBoxes)
   945  	s.Step(`^I sleep for (\d+) milliseconds for indexer to digest things down\.$`, sleptForNMilliSecsForIndexer)
   946  }