@algorandfoundation/algokit-utils 7.0.0-beta.17 → 7.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,57 @@ function getDeployTimeControl(approval, appSpec, templateVariableName, callConfi
40
40
  return !!abiCallConfig && abiCallConfig !== 'NEVER';
41
41
  });
42
42
  }
43
+ const BYTE_CBLOCK = 38;
44
+ const INT_CBLOCK = 32;
45
+ /**
46
+ * Get the offset of the last constant block at the beginning of the program
47
+ * This value is used to calculate the program counter for an ARC56 program that has a pcOffsetMethod of "cblocks"
48
+ *
49
+ * @param program The program to parse
50
+ * @returns The PC value of the opcode after the last constant block
51
+ */
52
+ function getConstantBlockOffset(program) {
53
+ const bytes = [...program];
54
+ const programSize = bytes.length;
55
+ bytes.shift(); // remove version
56
+ /** The PC of the opcode after the bytecblock */
57
+ let bytecblockOffset;
58
+ /** The PC of the opcode after the intcblock */
59
+ let intcblockOffset;
60
+ while (bytes.length > 0) {
61
+ /** The current byte from the beginning of the byte array */
62
+ const byte = bytes.shift();
63
+ // If the byte is a constant block...
64
+ if (byte === BYTE_CBLOCK || byte === INT_CBLOCK) {
65
+ const isBytecblock = byte === BYTE_CBLOCK;
66
+ /** The byte following the opcode is the number of values in the constant block */
67
+ const valuesRemaining = bytes.shift();
68
+ // Iterate over all the values in the constant block
69
+ for (let i = 0; i < valuesRemaining; i++) {
70
+ if (isBytecblock) {
71
+ /** The byte following the opcode is the length of the next element */
72
+ const length = bytes.shift();
73
+ bytes.splice(0, length);
74
+ }
75
+ else {
76
+ // intcblock is a uvarint, so we need to keep reading until we find the end (MSB is not set)
77
+ while ((bytes.shift() & 0x80) !== 0) {
78
+ // Do nothing...
79
+ }
80
+ }
81
+ }
82
+ if (isBytecblock)
83
+ bytecblockOffset = programSize - bytes.length - 1;
84
+ else
85
+ intcblockOffset = programSize - bytes.length - 1;
86
+ if (bytes[0] !== BYTE_CBLOCK && bytes[0] !== INT_CBLOCK) {
87
+ // if the next opcode isn't a constant block, we're done
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ return Math.max(bytecblockOffset ?? 0, intcblockOffset ?? 0);
93
+ }
43
94
  /** ARC-56/ARC-32 application client that allows you to manage calls and
44
95
  * state for a specific deployed instance of an app (with a known app ID). */
45
96
  class AppClient {
@@ -72,6 +123,19 @@ class AppClient {
72
123
  bare: this.getBareSendMethods(),
73
124
  };
74
125
  }
126
+ clone(params) {
127
+ return new AppClient({
128
+ appId: this._appId,
129
+ appSpec: this._appSpec,
130
+ algorand: this._algorand,
131
+ appName: this._appName,
132
+ defaultSender: this._defaultSender,
133
+ defaultSigner: this._defaultSigner,
134
+ approvalSourceMap: this._approvalSourceMap,
135
+ clearSourceMap: this._clearSourceMap,
136
+ ...params,
137
+ });
138
+ }
75
139
  /** Start a new `TransactionComposer` transaction group */
76
140
  newGroup() {
77
141
  return this._algorand.newGroup();
@@ -281,11 +345,19 @@ class AppClient {
281
345
  * @param isClearStateProgram Whether or not the code was running the clear state program (defaults to approval program)
282
346
  * @returns The new error, or if there was no logic error or source map then the wrapped error with source details
283
347
  */
284
- exposeLogicError(e, isClearStateProgram) {
348
+ async exposeLogicError(e, isClearStateProgram) {
349
+ const pcOffsetMethod = this._appSpec.sourceInfo?.[isClearStateProgram ? 'clear' : 'approval']?.pcOffsetMethod;
350
+ let program;
351
+ if (pcOffsetMethod === 'cblocks') {
352
+ // TODO: Cache this if we deploy the app and it's not updateable
353
+ const appInfo = await this._algorand.app.getById(this.appId);
354
+ program = isClearStateProgram ? appInfo.clearStateProgram : appInfo.approvalProgram;
355
+ }
285
356
  return AppClient.exposeLogicError(e, this._appSpec, {
286
357
  isClearStateProgram,
287
358
  approvalSourceMap: this._approvalSourceMap,
288
359
  clearSourceMap: this._clearSourceMap,
360
+ program,
289
361
  });
290
362
  }
291
363
  /**
@@ -360,17 +432,45 @@ class AppClient {
360
432
  * @returns The new error, or if there was no logic error or source map then the wrapped error with source details
361
433
  */
362
434
  static exposeLogicError(e, appSpec, details) {
363
- const { isClearStateProgram, approvalSourceMap, clearSourceMap } = details;
364
- if ((!isClearStateProgram && approvalSourceMap == undefined) || (isClearStateProgram && clearSourceMap == undefined))
365
- return e;
435
+ const { isClearStateProgram, approvalSourceMap, clearSourceMap, program } = details;
436
+ const sourceMap = isClearStateProgram ? clearSourceMap : approvalSourceMap;
366
437
  const errorDetails = LogicError.parseLogicError(e);
367
- const errorMessage = (isClearStateProgram ? appSpec.sourceInfo?.clear : appSpec.sourceInfo?.approval)?.find((s) => s?.pc?.includes(errorDetails?.pc ?? -1))?.errorMessage;
368
- if (errorDetails !== undefined && appSpec.source)
438
+ // Return the error if we don't have a PC
439
+ if (errorDetails === undefined || errorDetails?.pc === undefined)
440
+ return e;
441
+ /** The PC value to find in the ARC56 SourceInfo */
442
+ let arc56Pc = errorDetails?.pc;
443
+ const programSourceInfo = isClearStateProgram ? appSpec.sourceInfo?.clear : appSpec.sourceInfo?.approval;
444
+ /** The offset to apply to the PC if using the cblocks pc offset method */
445
+ let cblocksOffset = 0;
446
+ // If the program uses cblocks offset, then we need to adjust the PC accordingly
447
+ if (programSourceInfo?.pcOffsetMethod === 'cblocks') {
448
+ if (program === undefined)
449
+ throw new Error('Program bytes are required to calculate the ARC56 cblocks PC offset');
450
+ cblocksOffset = getConstantBlockOffset(program);
451
+ arc56Pc = errorDetails.pc - cblocksOffset;
452
+ }
453
+ // Find the source info for this PC and get the error message
454
+ const sourceInfo = programSourceInfo?.sourceInfo.find((s) => s.pc.includes(arc56Pc));
455
+ const errorMessage = sourceInfo?.errorMessage;
456
+ // If we have the source we can display the TEAL in the error message
457
+ if (appSpec.source) {
458
+ let getLineForPc = (inputPc) => sourceMap?.getLineForPc?.(inputPc);
459
+ // If the SourceMap is not defined, we need to provide our own function for going from a PC to TEAL based on ARC56 SourceInfo[]
460
+ if (sourceMap === undefined) {
461
+ getLineForPc = (inputPc) => {
462
+ const teal = programSourceInfo?.sourceInfo.find((s) => s.pc.includes(inputPc - cblocksOffset))?.teal;
463
+ if (teal === undefined)
464
+ return undefined;
465
+ return teal - 1;
466
+ };
467
+ }
369
468
  e = new LogicError(errorDetails, Buffer.from(isClearStateProgram ? appSpec.source.clear : appSpec.source.approval, 'base64')
370
469
  .toString()
371
470
  .split('\n'),
372
471
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373
- isClearStateProgram ? clearSourceMap : approvalSourceMap);
472
+ getLineForPc);
473
+ }
374
474
  if (errorMessage) {
375
475
  const appId = JSON.stringify(e).match(/(?<=app=)\d+/)?.[0] || '';
376
476
  const txId = JSON.stringify(e).match(/(?<=transaction )\S+(?=:)/)?.[0];
@@ -751,7 +851,7 @@ class AppClient {
751
851
  return await call();
752
852
  }
753
853
  catch (e) {
754
- throw this.exposeLogicError(e);
854
+ throw await this.exposeLogicError(e);
755
855
  }
756
856
  }
757
857
  getBoxMethods() {
@@ -1115,7 +1215,7 @@ class ApplicationClient {
1115
1215
  return { ...result, ...{ compiledApproval: approvalCompiled, compiledClear: clearCompiled } };
1116
1216
  }
1117
1217
  catch (e) {
1118
- throw this.exposeLogicError(e);
1218
+ throw await this.exposeLogicError(e);
1119
1219
  }
1120
1220
  }
1121
1221
  /**
@@ -1149,7 +1249,7 @@ class ApplicationClient {
1149
1249
  return { ...result, ...{ compiledApproval: approvalCompiled, compiledClear: clearCompiled } };
1150
1250
  }
1151
1251
  catch (e) {
1152
- throw this.exposeLogicError(e);
1252
+ throw await this.exposeLogicError(e);
1153
1253
  }
1154
1254
  }
1155
1255
  /**
@@ -1502,7 +1602,7 @@ class ApplicationClient {
1502
1602
  .toString()
1503
1603
  .split('\n'),
1504
1604
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1505
- isClear ? this._clearSourceMap : this._approvalSourceMap);
1605
+ (pc) => (isClear ? this._clearSourceMap : this._approvalSourceMap).getLineForPc(pc));
1506
1606
  else
1507
1607
  return e;
1508
1608
  }