-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathapp-client.ts
2417 lines (2193 loc) · 95.5 KB
/
app-client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import algosdk, { Address } from 'algosdk'
import { Buffer } from 'buffer'
import {
callApp,
compileTeal,
createApp,
getAppBoxNames,
getAppBoxValue,
getAppBoxValueFromABIType,
getAppGlobalState,
getAppLocalState,
updateApp,
} from '../app'
import { deployApp, getCreatorAppsByName, performTemplateSubstitution, replaceDeployTimeControlParams } from '../app-deploy'
import { Config } from '../config'
import { legacySendTransactionBridge } from '../transaction/legacy-bridge'
import { encodeTransactionNote, getSenderAddress } from '../transaction/transaction'
import { asJson, binaryStartsWith } from '../util'
import { TransactionSignerAccount } from './account'
import { AlgorandClientInterface } from './algorand-client-interface'
import { AlgoAmount } from './amount'
import {
ABIAppCallArg,
ABIAppCallArgs,
ABIReturn,
AppCallArgs,
AppCallTransactionResult,
AppCallType,
AppCompilationResult,
AppMetadata,
AppReference,
AppReturn,
AppState,
AppStorageSchema,
BoxName,
DELETABLE_TEMPLATE_NAME,
AppLookup as LegacyAppLookup,
OnSchemaBreak,
OnUpdate,
RawAppCallArgs,
SendAppTransactionResult,
TealTemplateParams,
UPDATABLE_TEMPLATE_NAME,
} from './app'
import {
ABIStruct,
Arc56Contract,
Arc56Method,
getABIDecodedValue,
getABIEncodedValue,
getABITupleFromABIStruct,
getArc56Method,
getArc56ReturnValue,
ProgramSourceInfo,
StorageKey,
StorageMap,
} from './app-arc56'
import { AppLookup } from './app-deployer'
import { AppManager, BoxIdentifier } from './app-manager'
import { AppSpec, arc32ToArc56 } from './app-spec'
import {
AppCallMethodCall,
AppCallParams,
AppDeleteMethodCall,
AppDeleteParams,
AppMethodCall,
AppMethodCallTransactionArgument,
AppUpdateMethodCall,
AppUpdateParams,
CommonAppCallParams,
PaymentParams,
} from './composer'
import { Expand } from './expand'
import { EventType } from './lifecycle-events'
import { LogicError } from './logic-error'
import { SendParams, SendTransactionFrom, SendTransactionParams, TransactionNote } from './transaction'
import ABIMethod = algosdk.ABIMethod
import ABIMethodParams = algosdk.ABIMethodParams
import ABIType = algosdk.ABIType
import ABIValue = algosdk.ABIValue
import Algodv2 = algosdk.Algodv2
import AtomicTransactionComposer = algosdk.AtomicTransactionComposer
import getApplicationAddress = algosdk.getApplicationAddress
import Indexer = algosdk.Indexer
import OnApplicationComplete = algosdk.OnApplicationComplete
import SourceMap = algosdk.ProgramSourceMap
import SuggestedParams = algosdk.SuggestedParams
import TransactionSigner = algosdk.TransactionSigner
/** Configuration to resolve app by creator and name `getCreatorAppsByName` */
export type ResolveAppByCreatorAndNameBase = {
/** The address of the app creator account to resolve the app by */
creatorAddress: Address | string
/** The optional name override to resolve the app by within the creator account (default: uses the name in the ABI contract) */
name?: string
/** The mechanism to find an existing app instance metadata for the given creator and name; either:
* * An indexer instance to search the creator account apps; or
* * The cached value of the existing apps for the given creator from `getCreatorAppsByName`
*/
findExistingUsing: Indexer | LegacyAppLookup
}
/** Configuration to resolve app by creator and name `getCreatorAppsByName` */
export type ResolveAppByCreatorAndName = ResolveAppByCreatorAndNameBase & {
/** How the app ID is resolved, either by `'id'` or `'creatorAndName'`; must be `'creatorAndName'` if you want to use `deploy` */
resolveBy: 'creatorAndName'
}
/** Configuration to resolve app by ID */
export interface ResolveAppByIdBase {
/** The id of an existing app to call using this client, or 0 if the app hasn't been created yet */
id: number | bigint
/** The optional name to use to mark the app when deploying `ApplicationClient.deploy` (default: uses the name in the ABI contract) */
name?: string
}
export interface ResolveAppById extends ResolveAppByIdBase {
/** How the app ID is resolved, either by `'id'` or `'creatorAndName'`; must be `'creatorAndName'` if you want to use `deploy` */
resolveBy: 'id'
}
/** The details of an AlgoKit Utils deployed app */
export type AppDetailsBase = {
/** Default sender to use for transactions issued by this application client */
sender?: SendTransactionFrom
/** Default suggested params object to use */
params?: SuggestedParams
/** Optionally provide any deploy-time parameters to replace in the TEAL code; if specified here will get
* used in calls to `deploy`, `create` and `update` unless overridden in those calls
*/
deployTimeParams?: TealTemplateParams
}
/** The details of an AlgoKit Utils deployed app */
export type AppDetails = AppDetailsBase & (ResolveAppById | ResolveAppByCreatorAndName)
/** The details of an ARC-0032 app spec specified, AlgoKit Utils deployed app */
export type AppSpecAppDetailsBase = {
/** The ARC-0032 application spec as either:
* * Parsed JSON `AppSpec`
* * Raw JSON string
*/
app: AppSpec | string
}
/** The details of an ARC-0032 app spec specified, AlgoKit Utils deployed app by id*/
export type AppSpecAppDetailsById = AppSpecAppDetailsBase & AppDetailsBase & ResolveAppByIdBase
/** The details of an ARC-0032 app spec specified, AlgoKit Utils deployed app by creator and name*/
export type AppSpecAppDetailsByCreatorAndName = AppSpecAppDetailsBase & AppDetailsBase & ResolveAppByCreatorAndNameBase
/** The details of an ARC-0032 app spec specified, AlgoKit Utils deployed app */
export type AppSpecAppDetails = AppSpecAppDetailsBase & AppDetails
/** Core parameters to pass into ApplicationClient.deploy */
export interface AppClientDeployCoreParams {
/** The version of the contract, uses "1.0" by default */
version?: string
/** The optional sender to send the transaction from, will use the application client's default sender by default if specified */
sender?: SendTransactionFrom
/** Parameters to control transaction sending */
sendParams?: Omit<SendTransactionParams, 'skipSending' | 'skipWaiting'>
/** Whether or not to allow updates in the contract using the deploy-time updatability control if present in your contract.
* If this is not specified then it will automatically be determined based on the AppSpec definition
**/
allowUpdate?: boolean
/** Whether or not to allow deletes in the contract using the deploy-time deletability control if present in your contract.
* If this is not specified then it will automatically be determined based on the AppSpec definition
**/
allowDelete?: boolean
/** What action to perform if a schema break is detected */
onSchemaBreak?: 'replace' | 'fail' | 'append' | OnSchemaBreak
/** What action to perform if a TEAL update is detected */
onUpdate?: 'update' | 'replace' | 'append' | 'fail' | OnUpdate
}
/** Call interface parameters to pass into ApplicationClient.deploy */
export interface AppClientDeployCallInterfaceParams {
/** Any deploy-time parameters to replace in the TEAL code */
deployTimeParams?: TealTemplateParams
/** Any args to pass to any create transaction that is issued as part of deployment */
createArgs?: AppClientCallArgs
/** Override the on-completion action for the create call; defaults to NoOp */
createOnCompleteAction?: Exclude<AppCallType, 'clear_state'> | Exclude<OnApplicationComplete, OnApplicationComplete.ClearStateOC>
/** Any args to pass to any update transaction that is issued as part of deployment */
updateArgs?: AppClientCallArgs
/** Any args to pass to any delete transaction that is issued as part of deployment */
deleteArgs?: AppClientCallArgs
}
/** Parameters to pass into ApplicationClient.deploy */
export interface AppClientDeployParams extends AppClientDeployCoreParams, AppClientDeployCallInterfaceParams {
/** Any overrides for the storage schema to request for the created app; by default the schema indicated by the app spec is used. */
schema?: Partial<AppStorageSchema>
}
export type AppClientCallRawArgs = RawAppCallArgs
export interface AppClientCallABIArgs extends Omit<ABIAppCallArgs, 'method'> {
/** If calling an ABI method then either the name of the method, or the ABI signature */
method: string
}
/** The arguments to pass to an Application Client smart contract call */
export type AppClientCallArgs = AppClientCallRawArgs | AppClientCallABIArgs
/** Common (core) parameters to construct a ApplicationClient contract call */
export interface AppClientCallCoreParams {
/** The optional sender to send the transaction from, will use the application client's default sender by default if specified */
sender?: SendTransactionFrom
/** The transaction note for the smart contract call */
note?: TransactionNote
/** Parameters to control transaction sending */
sendParams?: SendTransactionParams
}
/** Parameters to construct a ApplicationClient contract call */
export type AppClientCallParams = AppClientCallArgs & AppClientCallCoreParams
/** Parameters to construct a ApplicationClient clear state contract call */
export type AppClientClearStateParams = AppClientCallRawArgs & AppClientCallCoreParams
export interface AppClientCompilationParams {
/** Any deploy-time parameters to replace in the TEAL code */
deployTimeParams?: TealTemplateParams
/** Whether or not the contract should have deploy-time immutability control set, undefined = ignore */
updatable?: boolean
/** Whether or not the contract should have deploy-time permanence control set, undefined = ignore */
deletable?: boolean
}
/** On-complete action parameter for creating a contract using ApplicationClient */
export type AppClientCreateOnComplete = {
/** Override the on-completion action for the create call; defaults to NoOp */
onCompleteAction?: Exclude<AppCallType, 'clear_state'> | Exclude<OnApplicationComplete, OnApplicationComplete.ClearStateOC>
}
/** Parameters for creating a contract using ApplicationClient */
export type AppClientCreateParams = AppClientCallParams &
AppClientCompilationParams &
AppClientCreateOnComplete & {
/** Any overrides for the storage schema to request for the created app; by default the schema indicated by the app spec is used. */
schema?: Partial<AppStorageSchema>
}
/** Parameters for updating a contract using ApplicationClient */
export type AppClientUpdateParams = AppClientCallParams & AppClientCompilationParams
/** Parameters for funding an app account */
export interface FundAppAccountParams {
amount: AlgoAmount
/** The optional sender to send the transaction from, will use the application client's default sender by default if specified */
sender?: SendTransactionFrom
/** The transaction note for the smart contract call */
note?: TransactionNote
/** Parameters to control transaction sending */
sendParams?: SendTransactionParams
}
/** Source maps for an Algorand app */
export interface AppSourceMaps {
/** The source map of the approval program */
approvalSourceMap: SourceMapExport
/** The source map of the clear program */
clearSourceMap: SourceMapExport
}
export interface SourceMapExport {
version: number
sources: string[]
names: string[]
mappings: string
}
/**
* The result of asking an `AppClient` to compile a program.
*
* Always contains the compiled bytecode, and may contain the result of compiling TEAL (including sourcemap) if it was available.
*/
export interface AppClientCompilationResult extends Partial<AppCompilationResult> {
/** The compiled bytecode of the approval program, ready to deploy to algod */
approvalProgram: Uint8Array
/** The compiled bytecode of the clear state program, ready to deploy to algod */
clearStateProgram: Uint8Array
}
/**
* Determines deploy time control (UPDATABLE, DELETABLE) value by inspecting application specification
* @param approval TEAL Approval program, not the base64 version found on the appSpec
* @param appSpec Application Specification
* @param templateVariableName Template variable
* @param callConfigKey Call config type
* @returns true if applicable call config is found, false if not found or undefined if variable not present
*/
function getDeployTimeControl(
approval: string,
appSpec: AppSpec,
templateVariableName: string,
callConfigKey: 'update_application' | 'delete_application',
): boolean | undefined {
// variable not present, so unknown control value
if (!approval.includes(templateVariableName)) return undefined
// a bare call for specified CallConfig is present and configured
const bareCallConfig = appSpec.bare_call_config[callConfigKey]
if (!!bareCallConfig && bareCallConfig !== 'NEVER') return true
// an ABI call for specified CallConfig is present and configured
return Object.values(appSpec.hints).some((h) => {
const abiCallConfig = h.call_config[callConfigKey]
return !!abiCallConfig && abiCallConfig !== 'NEVER'
})
}
/** Parameters to create an app client */
export interface AppClientParams {
/** The ID of the app instance this client should make calls against. */
appId: bigint
/** The ARC-56 or ARC-32 application spec as either:
* * Parsed JSON ARC-56 `Contract`
* * Parsed JSON ARC-32 `AppSpec`
* * Raw JSON string (in either ARC-56 or ARC-32 format)
*/
appSpec: Arc56Contract | AppSpec | string
/** An `AlgorandClient` instance */
algorand: AlgorandClientInterface
/**
* Optional override for the app name; used for on-chain metadata and lookups.
* Defaults to the ARC-32/ARC-56 app spec name
*/
appName?: string
/** Optional address to use for the account to use as the default sender for calls. */
defaultSender?: Address | string
/** Optional signer to use as the default signer for default sender calls (if not specified then the signer will be resolved from `AlgorandClient`). */
defaultSigner?: TransactionSigner
/** Optional source map for the approval program */
approvalSourceMap?: SourceMap
/** Optional source map for the clear state program */
clearSourceMap?: SourceMap
}
/** Parameters to clone an app client */
export type CloneAppClientParams = Expand<Partial<Omit<AppClientParams, 'algorand' | 'appSpec'>>>
/** onComplete parameter for a non-update app call */
export type CallOnComplete = {
/** On-complete of the call; defaults to no-op */
onComplete?: Exclude<OnApplicationComplete, OnApplicationComplete.UpdateApplicationOC>
}
/** AppClient common parameters for a bare app call */
export type AppClientBareCallParams = Expand<
Omit<CommonAppCallParams, 'appId' | 'sender' | 'onComplete'> & {
/** The address of the account sending the transaction, if undefined then the app client's defaultSender is used. */
sender?: Address | string
}
>
/** AppClient common parameters for an ABI method call */
export type AppClientMethodCallParams = Expand<
Omit<CommonAppCallParams, 'appId' | 'sender' | 'method' | 'args'> & {
/** The address of the account sending the transaction, if undefined then the app client's defaultSender is used. */
sender?: Address | string
/** The method name or method signature to call if an ABI call is being emitted
* @example Method name
* `my_method`
* @example Method signature
* `my_method(unit64,string)bytes`
*/
method: string
/** Arguments to the ABI method, either:
* * An ABI value
* * An ARC-56 struct
* * A transaction with explicit signer
* * A transaction (where the signer will be automatically assigned)
* * An unawaited transaction (e.g. from algorand.createTransaction.transactionType())
* * Another method call (via method call params object)
* * undefined (this represents a placeholder for either a default argument or a transaction argument that is fulfilled by another method call argument)
*/
args?: (ABIValue | ABIStruct | AppMethodCallTransactionArgument | undefined)[]
}
>
/** Parameters for funding an app account */
export type FundAppParams = Expand<
Omit<PaymentParams, 'receiver' | 'sender'> &
SendParams & {
/** The optional sender to send the transaction from, will use the application client's default sender by default if specified */
sender?: Address | string
}
>
/** Resolve an app client instance by looking up an app created by the given creator with the given name */
export type ResolveAppClientByCreatorAndName = Expand<
Omit<AppClientParams, 'appId'> & {
/** The address of the creator account for the app */
creatorAddress: Address | string
/** An optional cached app lookup that matches a name to on-chain details;
* either this is needed or indexer is required to be passed in to this `ClientManager` on construction.
*/
appLookupCache?: AppLookup
/** Whether or not to ignore the `AppDeployer` lookup cache and force an on-chain lookup, default: use any cached value */
ignoreCache?: boolean
}
>
/** Resolve an app client instance by looking up the current network. */
export type ResolveAppClientByNetwork = Expand<Omit<AppClientParams, 'appId'>>
const BYTE_CBLOCK = 38
const INT_CBLOCK = 32
/**
* Get the offset of the last constant block at the beginning of the program
* This value is used to calculate the program counter for an ARC56 program that has a pcOffsetMethod of "cblocks"
*
* @param program The program to parse
* @returns The PC value of the opcode after the last constant block
*/
function getConstantBlockOffset(program: Uint8Array) {
const bytes = [...program]
const programSize = bytes.length
bytes.shift() // remove version
/** The PC of the opcode after the bytecblock */
let bytecblockOffset: number | undefined
/** The PC of the opcode after the intcblock */
let intcblockOffset: number | undefined
while (bytes.length > 0) {
/** The current byte from the beginning of the byte array */
const byte = bytes.shift()!
// If the byte is a constant block...
if (byte === BYTE_CBLOCK || byte === INT_CBLOCK) {
const isBytecblock = byte === BYTE_CBLOCK
/** The byte following the opcode is the number of values in the constant block */
const valuesRemaining = bytes.shift()!
// Iterate over all the values in the constant block
for (let i = 0; i < valuesRemaining; i++) {
if (isBytecblock) {
/** The byte following the opcode is the length of the next element */
const length = bytes.shift()!
bytes.splice(0, length)
} else {
// intcblock is a uvarint, so we need to keep reading until we find the end (MSB is not set)
while ((bytes.shift()! & 0x80) !== 0) {
// Do nothing...
}
}
}
if (isBytecblock) bytecblockOffset = programSize - bytes.length - 1
else intcblockOffset = programSize - bytes.length - 1
if (bytes[0] !== BYTE_CBLOCK && bytes[0] !== INT_CBLOCK) {
// if the next opcode isn't a constant block, we're done
break
}
}
}
return Math.max(bytecblockOffset ?? 0, intcblockOffset ?? 0)
}
/** ARC-56/ARC-32 application client that allows you to manage calls and
* state for a specific deployed instance of an app (with a known app ID). */
export class AppClient {
private _appId: bigint
private _appAddress: Address
private _appName: string
private _appSpec: Arc56Contract
private _algorand: AlgorandClientInterface
private _defaultSender?: Address
private _defaultSigner?: TransactionSigner
private _approvalSourceMap: SourceMap | undefined
private _clearSourceMap: SourceMap | undefined
private _localStateMethods: (address: string | Address) => ReturnType<AppClient['getStateMethods']>
private _globalStateMethods: ReturnType<AppClient['getStateMethods']>
private _boxStateMethods: ReturnType<AppClient['getBoxMethods']>
private _paramsMethods: ReturnType<AppClient['getMethodCallParamsMethods']> & {
/** Interact with bare (raw) call parameters */ bare: ReturnType<AppClient['getBareParamsMethods']>
}
private _createTransactionsMethods: ReturnType<AppClient['getMethodCallCreateTransactionMethods']> & {
/** Interact with bare (raw) call transactions */ bare: ReturnType<AppClient['getBareCreateTransactionMethods']>
}
private _sendMethods: ReturnType<AppClient['getMethodCallSendMethods']> & {
/** Interact with bare (raw) calls */ bare: ReturnType<AppClient['getBareSendMethods']>
}
constructor(params: AppClientParams) {
this._appId = params.appId
this._appAddress = algosdk.getApplicationAddress(this._appId)
this._appSpec = AppClient.normaliseAppSpec(params.appSpec)
this._appName = params.appName ?? this._appSpec.name
this._algorand = params.algorand
this._defaultSender = typeof params.defaultSender === 'string' ? Address.fromString(params.defaultSender) : params.defaultSender
this._defaultSigner = params.defaultSigner
this._approvalSourceMap = params.approvalSourceMap
this._clearSourceMap = params.clearSourceMap
this._localStateMethods = (address: string | Address) =>
this.getStateMethods(
() => this.getLocalState(address),
() => this._appSpec.state.keys.local,
() => this._appSpec.state.maps.local,
)
this._globalStateMethods = this.getStateMethods(
() => this.getGlobalState(),
() => this._appSpec.state.keys.global,
() => this._appSpec.state.maps.global,
)
this._boxStateMethods = this.getBoxMethods()
this._paramsMethods = {
...this.getMethodCallParamsMethods(),
/** Get parameters to define bare (raw) transactions to the current app */
bare: this.getBareParamsMethods(),
}
this._createTransactionsMethods = {
...this.getMethodCallCreateTransactionMethods(),
/** Get transactions for bare (raw) calls to the current app */
bare: this.getBareCreateTransactionMethods(),
}
this._sendMethods = {
...this.getMethodCallSendMethods(),
/** Send bare (raw) transactions to the current app */
bare: this.getBareSendMethods(),
}
}
/**
* Clone this app client with different params
*
* @param params The params to use for the the cloned app client. Omit a param to keep the original value. Set a param to override the original value. Setting to undefined will clear the original value.
* @returns A new app client with the altered params
*/
public clone(params: CloneAppClientParams) {
return new AppClient({
appId: this._appId,
appSpec: this._appSpec,
algorand: this._algorand,
appName: this._appName,
defaultSender: this._defaultSender,
defaultSigner: this._defaultSigner,
approvalSourceMap: this._approvalSourceMap,
clearSourceMap: this._clearSourceMap,
...params,
})
}
/**
* Returns a new `AppClient` client, resolving the app by creator address and name
* using AlgoKit app deployment semantics (i.e. looking for the app creation transaction note).
* @param params The parameters to create the app client
*/
public static async fromCreatorAndName(params: ResolveAppClientByCreatorAndName) {
const appSpec = AppClient.normaliseAppSpec(params.appSpec)
const appLookup =
params.appLookupCache ?? (await params.algorand.appDeployer.getCreatorAppsByName(params.creatorAddress, params.ignoreCache))
const appMetadata = appLookup.apps[params.appName ?? appSpec.name]
if (!appMetadata) {
throw new Error(`App not found for creator ${params.creatorAddress} and name ${params.appName ?? appSpec.name}`)
}
return new AppClient({
...params,
algorand: params.algorand,
appId: appMetadata.appId,
})
}
/**
* Returns an `AppClient` instance for the current network based on
* pre-determined network-specific app IDs specified in the ARC-56 app spec.
*
* If no IDs are in the app spec or the network isn't recognised, an error is thrown.
* @param params The parameters to create the app client
*/
public static async fromNetwork(params: ResolveAppClientByNetwork): Promise<AppClient> {
const network = await params.algorand.client.network()
const appSpec = AppClient.normaliseAppSpec(params.appSpec)
const networkNames = [network.genesisHash]
if (network.isLocalNet) networkNames.push('localnet')
if (network.isTestNet) networkNames.push('testnet')
if (network.isMainNet) networkNames.push('mainnet')
const availableAppSpecNetworks = Object.keys(appSpec.networks ?? {})
const networkIndex = availableAppSpecNetworks.findIndex((n) => networkNames.includes(n))
if (networkIndex === -1) {
throw new Error(`No app ID found for network ${asJson(networkNames)} in the app spec`)
}
const appId = BigInt(appSpec.networks![networkIndex].appID)
return new AppClient({ ...params, appId, appSpec })
}
/**
* Takes a string or parsed JSON object that could be ARC-32 or ARC-56 format and
* normalises it into a parsed ARC-56 contract object.
* @param spec The spec to normalise
* @returns The normalised ARC-56 contract object
*/
public static normaliseAppSpec(spec: Arc56Contract | AppSpec | string): Arc56Contract {
const parsedSpec = typeof spec === 'string' ? (JSON.parse(spec) as AppSpec | Arc56Contract) : spec
const appSpec = 'hints' in parsedSpec ? arc32ToArc56(parsedSpec) : parsedSpec
return appSpec
}
/** The ID of the app instance this client is linked to. */
public get appId() {
return this._appId
}
/** The app address of the app instance this client is linked to. */
public get appAddress() {
return this._appAddress
}
/** The name of the app (from the ARC-32 / ARC-56 app spec or override). */
public get appName() {
return this._appName
}
/** The ARC-56 app spec being used */
public get appSpec(): Arc56Contract {
return this._appSpec
}
/** A reference to the underlying `AlgorandClient` this app client is using. */
public get algorand(): AlgorandClientInterface {
return this._algorand
}
/** Get parameters to create transactions for the current app.
*
* A good mental model for this is that these parameters represent a deferred transaction creation.
* @example Create a transaction in the future using Algorand Client
* ```typescript
* const myMethodCall = appClient.params.call({method: 'my_method', args: [123, 'hello']})
* // ...
* await algorand.send.AppMethodCall(myMethodCall)
* ```
* @example Define a nested transaction as an ABI argument
* ```typescript
* const myMethodCall = appClient.params.call({method: 'my_method', args: [123, 'hello']})
* await appClient.send.call({method: 'my_method2', args: [myMethodCall]})
* ```
*/
public get params() {
return this._paramsMethods
}
/** Create transactions for the current app */
public get createTransaction() {
return this._createTransactionsMethods
}
/** Send transactions to the current app */
public get send() {
return this._sendMethods
}
/** Get state (local, global, box) from the current app */
public get state() {
return {
/**
* Methods to access local state for the current app
* @param address The address of the account to get the local state for
*/
local: this._localStateMethods,
/**
* Methods to access global state for the current app
*/
global: this._globalStateMethods,
/**
* Methods to access box storage for the current app
*/
box: this._boxStateMethods,
}
}
/**
* Funds Algo into the app account for this app.
*
* An alias for `appClient.send.fundAppAccount(params)`.
* @param params The parameters for the funding transaction
* @returns The result of the funding
*/
public async fundAppAccount(params: FundAppParams) {
return this.send.fundAppAccount(params)
}
/**
* Returns raw global state for the current app.
* @returns The global state
*/
public async getGlobalState(): Promise<AppState> {
return await this._algorand.app.getGlobalState(this.appId)
}
/**
* Returns raw local state for the given account address.
* @param address The address of the account to get the local state for
* @returns The local state
*/
public async getLocalState(address: Address | string): Promise<AppState> {
return await this._algorand.app.getLocalState(this.appId, address)
}
/**
* Returns the names of all current boxes for the current app.
* @returns The names of the boxes
*/
public async getBoxNames(): Promise<BoxName[]> {
return await this._algorand.app.getBoxNames(this.appId)
}
/**
* Returns the value of the given box for the current app.
* @param name The identifier of the box to return
* @returns The current box value as a byte array
*/
public async getBoxValue(name: BoxIdentifier): Promise<Uint8Array> {
return await this._algorand.app.getBoxValue(this.appId, name)
}
/**
* Returns the value of the given box for the current app.
* @param name The identifier of the box to return
* @param type
* @returns The current box value as a byte array
*/
public async getBoxValueFromABIType(name: BoxIdentifier, type: ABIType): Promise<ABIValue> {
return await this._algorand.app.getBoxValueFromABIType({
appId: this.appId,
boxName: name,
type,
})
}
/**
* Returns the values of all current boxes for the current app.
* Note: This will issue multiple HTTP requests (one per box) and it's not an atomic operation so values may be out of sync.
* @param filter Optional filter to filter which boxes' values are returned
* @returns The (name, value) pair of the boxes with values as raw byte arrays
*/
public async getBoxValues(filter?: (name: BoxName) => boolean): Promise<{ name: BoxName; value: Uint8Array }[]> {
const names = (await this.getBoxNames()).filter(filter ?? ((_) => true))
const values = await this._algorand.app.getBoxValues(
this.appId,
names.map((name) => name.nameRaw),
)
return names.map((name, i) => ({ name, value: values[i] }))
}
/**
* Returns the values of all current boxes for the current app decoded using an ABI Type.
* Note: This will issue multiple HTTP requests (one per box) and it's not an atomic operation so values may be out of sync.
* @param type The ABI type to decode the values with
* @param filter Optional filter to filter which boxes' values are returned
* @returns The (name, value) pair of the boxes with values as the ABI Value
*/
public async getBoxValuesFromABIType(type: ABIType, filter?: (name: BoxName) => boolean): Promise<{ name: BoxName; value: ABIValue }[]> {
const names = (await this.getBoxNames()).filter(filter ?? ((_) => true))
const values = await this._algorand.app.getBoxValuesFromABIType({
appId: this.appId,
boxNames: names.map((name) => name.nameRaw),
type,
})
return names.map((name, i) => ({ name, value: values[i] }))
}
/**
* Takes an error that may include a logic error from a call to the current app and re-exposes the
* error to include source code information via the source map and ARC-56 spec.
* @param e The error to parse
* @param isClearStateProgram Whether or not the code was running the clear state program (defaults to approval program)
* @returns The new error, or if there was no logic error or source map then the wrapped error with source details
*/
public async exposeLogicError(e: Error, isClearStateProgram?: boolean): Promise<Error> {
const pcOffsetMethod = this._appSpec.sourceInfo?.[isClearStateProgram ? 'clear' : 'approval']?.pcOffsetMethod
let program: Uint8Array | undefined
if (pcOffsetMethod === 'cblocks') {
// TODO: Cache this if we deploy the app and it's not updateable
const appInfo = await this._algorand.app.getById(this.appId)
program = isClearStateProgram ? appInfo.clearStateProgram : appInfo.approvalProgram
}
return AppClient.exposeLogicError(e, this._appSpec, {
isClearStateProgram,
approvalSourceMap: this._approvalSourceMap,
clearSourceMap: this._clearSourceMap,
program,
})
}
/**
* Export the current source maps for the app.
* @returns The source maps
*/
public exportSourceMaps(): AppSourceMaps {
if (!this._approvalSourceMap || !this._clearSourceMap) {
throw new Error(
"Unable to export source maps; they haven't been loaded into this client - you need to call create, update, or deploy first",
)
}
return {
approvalSourceMap: this._approvalSourceMap,
clearSourceMap: this._clearSourceMap,
}
}
/**
* Import source maps for the app.
* @param sourceMaps The source maps to import
*/
public importSourceMaps(sourceMaps: AppSourceMaps) {
this._approvalSourceMap = new SourceMap(sourceMaps.approvalSourceMap)
this._clearSourceMap = new SourceMap(sourceMaps.clearSourceMap)
}
/**
* Returns the ABI Method spec for the given method string for the app represented by this application client instance
* @param methodNameOrSignature The method name or method signature to call if an ABI call is being emitted.
* e.g. `my_method` or `my_method(unit64,string)bytes`
* @returns A tuple with: [ARC-56 `Method`, algosdk `ABIMethod`]
*/
public getABIMethod(methodNameOrSignature: string) {
return getArc56Method(methodNameOrSignature, this._appSpec)
}
/**
* Checks for decode errors on the SendAppTransactionResult and maps the return value to the specified type
* on the ARC-56 method, replacing the `return` property with the decoded type.
*
* If the return type is an ARC-56 struct then the struct will be returned.
*
* @param result The SendAppTransactionResult to be mapped
* @param method The method that was called
* @returns The smart contract response with an updated return value
*/
public async processMethodCallReturn<
TReturn extends Uint8Array | ABIValue | ABIStruct | undefined,
TResult extends SendAppTransactionResult = SendAppTransactionResult,
>(result: Promise<TResult> | TResult, method: Arc56Method): Promise<Omit<TResult, 'return'> & AppReturn<TReturn>> {
const resultValue = await result
return { ...resultValue, return: getArc56ReturnValue(resultValue.return, method, this._appSpec.structs) }
}
/**
* Compiles the approval and clear state programs (if TEAL templates provided),
* performing any provided deploy-time parameter replacement and stores
* the source maps.
*
* If no TEAL templates provided it will use any byte code provided in the app spec.
*
* Will store any generated source maps for later use in debugging.
*/
public async compile(compilation?: AppClientCompilationParams) {
const result = await AppClient.compile(this._appSpec, this._algorand.app, compilation)
if (result.compiledApproval) {
this._approvalSourceMap = result.compiledApproval.sourceMap
}
if (result.compiledClear) {
this._clearSourceMap = result.compiledClear.sourceMap
}
return result
}
/**
* Takes an error that may include a logic error from a call to the current app and re-exposes the
* error to include source code information via the source map and ARC-56 spec.
* @param e The error to parse
* @param appSpec The app spec for the app
* @param details Additional information to inform the error
* @returns The new error, or if there was no logic error or source map then the wrapped error with source details
*/
public static exposeLogicError(
e: Error,
appSpec: Arc56Contract,
details: {
/** Whether or not the code was running the clear state program (defaults to approval program) */ isClearStateProgram?: boolean
/** Approval program source map */ approvalSourceMap?: SourceMap
/** Clear state program source map */ clearSourceMap?: SourceMap
/** program bytes */ program?: Uint8Array
/** ARC56 approval source info */ approvalSourceInfo?: ProgramSourceInfo
/** ARC56 clear source info */ clearSourceInfo?: ProgramSourceInfo
},
): Error {
const { isClearStateProgram, approvalSourceMap, clearSourceMap, program } = details
const sourceMap = isClearStateProgram ? clearSourceMap : approvalSourceMap
const errorDetails = LogicError.parseLogicError(e)
// Return the error if we don't have a PC
if (errorDetails === undefined || errorDetails?.pc === undefined) return e
/** The PC value to find in the ARC56 SourceInfo */
let arc56Pc = errorDetails?.pc
const programSourceInfo = isClearStateProgram ? appSpec.sourceInfo?.clear : appSpec.sourceInfo?.approval
/** The offset to apply to the PC if using the cblocks pc offset method */
let cblocksOffset = 0
// If the program uses cblocks offset, then we need to adjust the PC accordingly
if (programSourceInfo?.pcOffsetMethod === 'cblocks') {
if (program === undefined) throw new Error('Program bytes are required to calculate the ARC56 cblocks PC offset')
cblocksOffset = getConstantBlockOffset(program)
arc56Pc = errorDetails.pc - cblocksOffset
}
// Find the source info for this PC and get the error message
const sourceInfo = programSourceInfo?.sourceInfo.find((s) => s.pc.includes(arc56Pc))
const errorMessage = sourceInfo?.errorMessage
// If we have the source we can display the TEAL in the error message
if (appSpec.source) {
let getLineForPc = (inputPc: number) => sourceMap?.getLocationForPc?.(inputPc)?.line
// If the SourceMap is not defined, we need to provide our own function for going from a PC to TEAL based on ARC56 SourceInfo[]
if (sourceMap === undefined) {
getLineForPc = (inputPc: number) => {
const teal = programSourceInfo?.sourceInfo.find((s) => s.pc.includes(inputPc - cblocksOffset))?.teal
if (teal === undefined) return undefined
return teal - 1
}
}
e = new LogicError(
errorDetails,
Buffer.from(isClearStateProgram ? appSpec.source.clear : appSpec.source.approval, 'base64')
.toString()
.split('\n'),
getLineForPc,
)
}
if (errorMessage) {
const appId = asJson(e).match(/(?<=app=)\d+/)?.[0] || ''
const txId = asJson(e).match(/(?<=transaction )\S+(?=:)/)?.[0]
const error = new Error(`Runtime error when executing ${appSpec.name} (appId: ${appId}) in transaction ${txId}: ${errorMessage}`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).cause = e
return error
}
return e
}
/**
* Compiles the approval and clear state programs (if TEAL templates provided),
* performing any provided deploy-time parameter replacement and returns
* the compiled code and any compilation results (including source maps).
*
* If no TEAL templates provided it will use any byte code provided in the app spec.
*
* Will store any generated source maps for later use in debugging.
* @param appSpec The app spec for the app
* @param compilation Any compilation parameters to use
*/
public static async compile(
appSpec: Arc56Contract,
appManager: AppManager,
compilation?: AppClientCompilationParams,
): Promise<AppClientCompilationResult> {
const { deployTimeParams, updatable, deletable } = compilation ?? {}
if (!appSpec.source) {
if (!appSpec.byteCode?.approval || !appSpec.byteCode?.clear) {
throw new Error(`Attempt to compile app ${appSpec.name} without source or byteCode`)
}
return {
approvalProgram: Buffer.from(appSpec.byteCode.approval, 'base64') as Uint8Array,
clearStateProgram: Buffer.from(appSpec.byteCode.clear, 'base64') as Uint8Array,
}
}
const approvalTemplate = Buffer.from(appSpec.source.approval, 'base64').toString('utf-8')
const compiledApproval = await appManager.compileTealTemplate(approvalTemplate, deployTimeParams, {
updatable,
deletable,
})
const clearTemplate = Buffer.from(appSpec.source.clear, 'base64').toString('utf-8')