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 }