github.com/algorand/go-algorand-sdk@v1.24.0/future/dryrun.go (about)

     1  package future
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"strings"
    10  	"text/tabwriter"
    11  
    12  	"github.com/algorand/go-algorand-sdk/client/v2/algod"
    13  	"github.com/algorand/go-algorand-sdk/client/v2/common/models"
    14  	"github.com/algorand/go-algorand-sdk/crypto"
    15  	"github.com/algorand/go-algorand-sdk/types"
    16  )
    17  
    18  const (
    19  	defaultAppId uint64 = 1380011588
    20  
    21  	rejectMsg       = "REJECT"
    22  	defaultMaxWidth = 30
    23  )
    24  
    25  // CreateDryrun creates a DryrunRequest object from a client and slice of SignedTxn objects and a default configuration
    26  // Passed in as a pointer to a DryrunRequest object to use for extra parameters
    27  func CreateDryrun(client *algod.Client, txns []types.SignedTxn, dr *models.DryrunRequest, ctx context.Context) (drr models.DryrunRequest, err error) {
    28  	var (
    29  		apps   []types.AppIndex
    30  		assets []types.AssetIndex
    31  		accts  []types.Address
    32  	)
    33  
    34  	drr.Txns = txns
    35  
    36  	if dr != nil {
    37  		drr.LatestTimestamp = dr.LatestTimestamp
    38  		drr.Round = dr.Round
    39  		drr.ProtocolVersion = dr.ProtocolVersion
    40  		drr.Sources = dr.Sources
    41  	}
    42  
    43  	for _, t := range txns {
    44  		if t.Txn.Type != types.ApplicationCallTx {
    45  			continue
    46  		}
    47  
    48  		accts = append(accts, t.Txn.Sender)
    49  
    50  		accts = append(accts, t.Txn.Accounts...)
    51  		apps = append(apps, t.Txn.ForeignApps...)
    52  
    53  		for _, aidx := range t.Txn.ForeignApps {
    54  			accts = append(accts, crypto.GetApplicationAddress(uint64(aidx)))
    55  		}
    56  
    57  		assets = append(assets, t.Txn.ForeignAssets...)
    58  
    59  		if t.Txn.ApplicationID == 0 {
    60  			drr.Apps = append(drr.Apps, models.Application{
    61  				Id: defaultAppId,
    62  				Params: models.ApplicationParams{
    63  					Creator:           t.Txn.Sender.String(),
    64  					ApprovalProgram:   t.Txn.ApprovalProgram,
    65  					ClearStateProgram: t.Txn.ClearStateProgram,
    66  					LocalStateSchema: models.ApplicationStateSchema{
    67  						NumByteSlice: t.Txn.LocalStateSchema.NumByteSlice,
    68  						NumUint:      t.Txn.LocalStateSchema.NumUint,
    69  					},
    70  					GlobalStateSchema: models.ApplicationStateSchema{
    71  						NumByteSlice: t.Txn.GlobalStateSchema.NumByteSlice,
    72  						NumUint:      t.Txn.GlobalStateSchema.NumUint,
    73  					},
    74  				},
    75  			})
    76  		} else {
    77  			apps = append(apps, t.Txn.ApplicationID)
    78  			accts = append(accts, crypto.GetApplicationAddress(uint64(t.Txn.ApplicationID)))
    79  		}
    80  	}
    81  
    82  	seenAssets := map[types.AssetIndex]bool{}
    83  	for _, assetId := range assets {
    84  		if _, ok := seenAssets[assetId]; ok {
    85  			continue
    86  		}
    87  
    88  		assetInfo, err := client.GetAssetByID(uint64(assetId)).Do(ctx)
    89  		if err != nil {
    90  			return drr, fmt.Errorf("failed to get asset %d: %+v", assetId, err)
    91  		}
    92  
    93  		addr, err := types.DecodeAddress(assetInfo.Params.Creator)
    94  		if err != nil {
    95  			return drr, fmt.Errorf("failed to decode creator adddress %s: %+v", assetInfo.Params.Creator, err)
    96  		}
    97  
    98  		accts = append(accts, addr)
    99  		seenAssets[assetId] = true
   100  	}
   101  
   102  	seenApps := map[types.AppIndex]bool{}
   103  	for _, appId := range apps {
   104  		if _, ok := seenApps[appId]; ok {
   105  			continue
   106  		}
   107  
   108  		appInfo, err := client.GetApplicationByID(uint64(appId)).Do(ctx)
   109  		if err != nil {
   110  			return drr, fmt.Errorf("failed to get application %d: %+v", appId, err)
   111  		}
   112  		drr.Apps = append(drr.Apps, appInfo)
   113  
   114  		creator, err := types.DecodeAddress(appInfo.Params.Creator)
   115  		if err != nil {
   116  			return drr, fmt.Errorf("failed to decode creator address %s: %+v", appInfo.Params.Creator, err)
   117  		}
   118  		accts = append(accts, creator)
   119  
   120  		seenApps[appId] = true
   121  	}
   122  
   123  	seenAccts := map[types.Address]bool{}
   124  	for _, acct := range accts {
   125  		if _, ok := seenAccts[acct]; ok {
   126  			continue
   127  		}
   128  		acctInfo, err := client.AccountInformation(acct.String()).Do(ctx)
   129  		if err != nil {
   130  			return drr, fmt.Errorf("failed to get application %s: %+v", acct, err)
   131  		}
   132  		drr.Accounts = append(drr.Accounts, acctInfo)
   133  		seenAccts[acct] = true
   134  	}
   135  
   136  	return
   137  }
   138  
   139  // StackPrinterConfig holds some configuration parameters for how to print a DryrunResponse stack trace
   140  type StackPrinterConfig struct {
   141  	MaxValueWidth   int  // Set the max width of the column, 0 is no max
   142  	TopOfStackFirst bool // Set the order of the stack values printed, true is top of stack (last pushed) first
   143  }
   144  
   145  // DefaultStackPrinterConfig returns a new StackPrinterConfig with reasonable defaults
   146  func DefaultStackPrinterConfig() StackPrinterConfig {
   147  	return StackPrinterConfig{MaxValueWidth: defaultMaxWidth, TopOfStackFirst: true}
   148  }
   149  
   150  type DryrunResponse struct {
   151  	Error           string            `json:"error"`
   152  	ProtocolVersion string            `json:"protocol-version"`
   153  	Txns            []DryrunTxnResult `json:"txns"`
   154  }
   155  
   156  func NewDryrunResponse(d models.DryrunResponse) (DryrunResponse, error) {
   157  	// Marshal and unmarshal to fix integer types.
   158  	b, err := json.Marshal(d)
   159  	if err != nil {
   160  		return DryrunResponse{}, err
   161  	}
   162  	return NewDryrunResponseFromJson(b)
   163  }
   164  
   165  func NewDryrunResponseFromJson(js []byte) (DryrunResponse, error) {
   166  	dr := DryrunResponse{}
   167  	err := json.Unmarshal(js, &dr)
   168  	return dr, err
   169  }
   170  
   171  type DryrunTxnResult struct {
   172  	models.DryrunTxnResult
   173  }
   174  
   175  // AppCallRejected returns true if the Application Call was rejected
   176  // for this transaction
   177  func (d *DryrunTxnResult) AppCallRejected() bool {
   178  	for _, m := range d.AppCallMessages {
   179  		if m == rejectMsg {
   180  			return true
   181  		}
   182  	}
   183  	return false
   184  }
   185  
   186  // LogicSigRejected returns true if the LogicSig was rejected
   187  // for this transaction
   188  func (d *DryrunTxnResult) LogicSigRejected() bool {
   189  	for _, m := range d.LogicSigMessages {
   190  		if m == rejectMsg {
   191  			return true
   192  		}
   193  	}
   194  	return false
   195  }
   196  
   197  type indexedScratchValue struct {
   198  	Idx int
   199  	Val models.TealValue
   200  }
   201  
   202  func scratchToString(prevScratch, currScratch []models.TealValue) string {
   203  	if len(currScratch) == 0 {
   204  		return ""
   205  	}
   206  
   207  	var newScratchVal *indexedScratchValue
   208  
   209  	prevScratchMap := map[indexedScratchValue]bool{}
   210  	for idx, psv := range prevScratch {
   211  		isv := indexedScratchValue{Idx: idx, Val: psv}
   212  		prevScratchMap[isv] = true
   213  	}
   214  
   215  	for idx, csv := range currScratch {
   216  		isv := indexedScratchValue{Idx: idx, Val: csv}
   217  		if _, ok := prevScratchMap[isv]; !ok {
   218  			newScratchVal = &isv
   219  		}
   220  	}
   221  
   222  	if newScratchVal == nil {
   223  		return ""
   224  	}
   225  
   226  	if len(newScratchVal.Val.Bytes) > 0 {
   227  		decoded, _ := base64.StdEncoding.DecodeString(newScratchVal.Val.Bytes)
   228  		return fmt.Sprintf("%d = %#x", newScratchVal.Idx, decoded)
   229  	}
   230  
   231  	return fmt.Sprintf("%d = %d", newScratchVal.Idx, newScratchVal.Val.Uint)
   232  }
   233  
   234  func stackToString(reverse bool, stack []models.TealValue) string {
   235  	elems := len(stack)
   236  	svs := make([]string, elems)
   237  	for idx, s := range stack {
   238  
   239  		svidx := idx
   240  		if reverse {
   241  			svidx = (elems - 1) - idx
   242  		}
   243  
   244  		if s.Type == 1 {
   245  			// Just returns empty string if there is an error, use it
   246  			decoded, _ := base64.StdEncoding.DecodeString(s.Bytes)
   247  			svs[svidx] = fmt.Sprintf("%#x", decoded)
   248  		} else {
   249  			svs[svidx] = fmt.Sprintf("%d", s.Uint)
   250  		}
   251  	}
   252  
   253  	return fmt.Sprintf("[%s]", strings.Join(svs, ", "))
   254  }
   255  
   256  func (d *DryrunTxnResult) trace(state []models.DryrunState, disassemmbly []string, spc StackPrinterConfig) string {
   257  	buff := bytes.NewBuffer(nil)
   258  	w := tabwriter.NewWriter(buff, 0, 0, 1, ' ', tabwriter.Debug)
   259  
   260  	fmt.Fprintln(w, "pc#\tln#\tsource\tscratch\tstack")
   261  	for idx, s := range state {
   262  
   263  		prevScratch := []models.TealValue{}
   264  		if idx > 0 {
   265  			prevScratch = state[idx-1].Scratch
   266  		}
   267  
   268  		src := disassemmbly[s.Line]
   269  		if s.Error != "" {
   270  			src = fmt.Sprintf("!! %s !!", s.Error)
   271  		}
   272  
   273  		srcLine := fmt.Sprintf("%d\t%d\t%s\t%s\t%s",
   274  			s.Pc, s.Line,
   275  			truncate(src, spc.MaxValueWidth),
   276  			truncate(scratchToString(prevScratch, s.Scratch), spc.MaxValueWidth),
   277  			truncate(stackToString(spc.TopOfStackFirst, s.Stack), spc.MaxValueWidth))
   278  
   279  		fmt.Fprintln(w, srcLine)
   280  	}
   281  	w.Flush()
   282  
   283  	return buff.String()
   284  }
   285  
   286  // GetAppCallTrace returns a string representing a stack trace for this transactions
   287  // application logic evaluation
   288  func (d *DryrunTxnResult) GetAppCallTrace(spc StackPrinterConfig) string {
   289  	return d.trace(d.AppCallTrace, d.Disassembly, spc)
   290  }
   291  
   292  // GetLogicSigTrace returns a string representing a stack trace for this transactions
   293  // logic signature evaluation
   294  func (d *DryrunTxnResult) GetLogicSigTrace(spc StackPrinterConfig) string {
   295  	return d.trace(d.LogicSigTrace, d.LogicSigDisassembly, spc)
   296  }
   297  
   298  func truncate(str string, width int) string {
   299  	if len(str) > width && width > 0 {
   300  		return str[:width] + "..."
   301  	}
   302  	return str
   303  }