Skip to content

Commit

Permalink
WIP-compiled contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmenzel committed Dec 3, 2024
1 parent 0fda7b5 commit 4137c7a
Show file tree
Hide file tree
Showing 29 changed files with 3,881 additions and 37 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev:examples": "tsx src/cli.ts build examples --output-awst --output-awst-json",
"dev:approvals": "rimraf tests/approvals/out && tsx src/cli.ts build tests/approvals --dry-run",
"dev:expected-output": "tsx src/cli.ts build tests/expected-output --dry-run",
"dev:testing": "tsx src/cli.ts build tests/approvals/arc4-encode-decode.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --log-level info --out-dir out/[name] --optimization-level=0",
"dev:testing": "tsx src/cli.ts build tests/approvals/precompiled-factory.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --log-level info --out-dir out/[name] --optimization-level=0",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
13 changes: 12 additions & 1 deletion packages/algo-ts/src/arc4/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseContract } from '../base-contract'
import { ctxMgr } from '../execution-context'
import { Uint64 } from '../primitives'
import { NoImplementation } from '../impl/errors'
import { bytes, Uint64 } from '../primitives'
import { DeliberateAny } from '../typescript-helpers'

export * from './encoded-types'
Expand Down Expand Up @@ -82,3 +83,13 @@ export function baremethod<TContract extends Contract>(config?: BareMethodConfig
return target
}
}

/**
* Returns the ARC4 method selector for a given ARC4 method signature. The method selector is the first
* 4 bytes of the SHA512/256 hash of the method signature.
* @param methodSignature An ARC4 method signature. Eg. `hello(string)string`. Must be a compile time constant.
* @returns The ARC4 method selector. Eg. `02BECE11`
*/
export function methodSelector(methodSignature: string): bytes {
throw new NoImplementation()
}
116 changes: 116 additions & 0 deletions packages/algo-ts/src/compiled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { BaseContract } from './base-contract'
import { NoImplementation } from './impl/errors'
import { LogicSig } from './logic-sig'
import { bytes, uint64 } from './primitives'
import { Account } from './reference'
import { ConstructorFor, DeliberateAny } from './typescript-helpers'

/**
* Provides compiled programs and state allocation values for a Contract. Created by calling `compile(ExampleContractType)`
*/
export type CompiledContract = {
/**
* Approval program pages for a contract, after template variables have been replaced and compiled to AVM bytecode
*/
approvalProgram: [bytes, bytes]
/**
* Clear state program pages for a contract, after template variables have been replaced and compiled to AVM bytecode
*/
clearStateProgram: [bytes, bytes]
/**
* By default, provides extra program pages required based on approval and clear state program size, can be overridden when calling `compile(ExampleContractType, { extraProgramPages: ... })`
*/
extraProgramPages: uint64
/**
* By default, provides global num uints based on contract state totals, can be overridden when calling `compile(ExampleContractType, { globalUints: ... })`
*/
globalUints: uint64
/**
* By default, provides global num bytes based on contract state totals, can be overridden when calling `compile(ExampleContractType, { globalBytes: ... })`
*/
globalBytes: uint64
/**
* By default, provides local num uints based on contract state totals, can be overridden when calling `compile(ExampleContractType, { localUints: ... })`
*/
localUints: uint64
/**
* By default, provides local num bytes based on contract state totals, can be overridden when calling `compile(ExampleContractType, { localBytes: ... })`
*/
localBytes: uint64
}

/**
* Provides account for a Logic Signature. Created by calling `compile(LogicSigType)`
*/
export type CompiledLogicSig = {
/**
* Address of a logic sig program, after template variables have been replaced and compiled to AVM bytecode
*/
account: Account
}

/**
* Options for compiling a contract
*/
type CompileContractOptions = {
/**
* Number of extra program pages, defaults to minimum required for contract
*/
extraProgramPages?: uint64
/**
* Number of global uint64s, defaults to value defined for contract
*/
globalUints?: uint64
/**
* Number of global bytes, defaults to value defined for contract
*/
globalBytes?: uint64
/**
* Number of local uint64s, defaults to value defined for contract
*/
localUints?: uint64
/**
* Number of local bytes, defaults to value defined for contract
*/
localBytes?: uint64
/**
* Template variables to substitute into the contract, key should be without the prefix, must evaluate to a compile time constant
* and match the type of the template var declaration
*/
templateVars?: Record<string, DeliberateAny>
/**
* Prefix to add to provided template vars, defaults to the prefix supplied on command line (which defaults to TMPL_)
*/
templateVarsPrefix?: string
}

/**
* Options for compiling a logic signature
*/
type CompileLogicSigOptions = {
/**
* Template variables to substitute into the contract, key should be without the prefix, must evaluate to a compile time constant
* and match the type of the template var declaration
*/
templateVars?: Record<string, DeliberateAny>
/**
* Prefix to add to provided template vars, defaults to the prefix supplied on command line (which defaults to TMPL_)
*/
templateVarsPrefix?: string
}

/**
* Compile a contract and return the resulting byte code for approval and clear state programs.
* @param contract The contract class to compile
* @param options Options for compiling the contract
*/
export function compile(contract: ConstructorFor<BaseContract>, options?: CompileContractOptions): CompiledContract
/**
* Compile a logic signature and return an account ready for signing transactions.
* @param logicSig The logic sig class to compile
* @param options Options for compiling the logic sig
*/
export function compile(logicSig: ConstructorFor<LogicSig>, options?: CompileLogicSigOptions): CompiledLogicSig
export function compile(artefact: ConstructorFor<BaseContract> | ConstructorFor<LogicSig>): CompiledLogicSig | CompiledContract {
throw new NoImplementation()
}
1 change: 1 addition & 0 deletions packages/algo-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { TransactionType } from './transactions'
export { LogicSig } from './logic-sig'
export { TemplateVar } from './template-var'
export { Base64, Ec, Ecdsa, VrfVerify } from './op-types'
export { compile, CompiledContract, CompiledLogicSig } from './compiled'
4 changes: 3 additions & 1 deletion packages/algo-ts/src/template-var.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NoImplementation } from './impl/errors'

export function TemplateVar<T>(variableName: string, prefix = 'TMPL_'): T {
throw new Error('TODO')
throw NoImplementation
}
1 change: 1 addition & 0 deletions packages/algo-ts/src/typescript-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type DeliberateAny = any
export type AnyFunction = (...args: DeliberateAny[]) => DeliberateAny
export type ConstructorFor<T, TArgs extends DeliberateAny[] = DeliberateAny[]> = new (...args: TArgs) => T
29 changes: 27 additions & 2 deletions src/awst/to-code-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,35 @@ export class ToCodeVisitor
return `${expression.target.accept(this)}${expression.op}`
}
visitCompiledContract(expression: nodes.CompiledContract): string {
throw new TodoError('Method not implemented.', { sourceLocation: expression.sourceLocation })
let overrides = Array.from(expression.allocationOverrides.entries())
.map(([f, v]) => `${f}=${v.accept(this)}`)
.join(', ')
if (overrides) {
overrides = `, ${overrides}`
}

let templateVars = Array.from(expression.templateVariables.entries())
.map(([n, v]) => `${n}=${v.accept(this)}`)
.join(', ')
if (templateVars) {
templateVars = `, ${templateVars}`
}

const prefix = expression.prefix ? `, prefix=${expression.prefix}` : ''

return `compile(${expression.contract.id}${overrides}${prefix}${templateVars}`
}
visitCompiledLogicSig(expression: nodes.CompiledLogicSig): string {
throw new TodoError('Method not implemented.', { sourceLocation: expression.sourceLocation })
let templateVars = Array.from(expression.templateVariables.entries())
.map(([n, v]) => `${n}=${v.accept(this)}`)
.join(', ')
if (templateVars) {
templateVars = `, ${templateVars}`
}

const prefix = expression.prefix ? `, prefix=${expression.prefix}` : ''

return `compile(${expression.logicSig.id}${prefix}${templateVars}`
}
visitLoopExit(statement: nodes.LoopExit): string[] {
throw new TodoError('Method not implemented.', { sourceLocation: statement.sourceLocation })
Expand Down
21 changes: 18 additions & 3 deletions src/awst/txn-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ export class TxnFieldData {
readonly wtype: wtypes.WType
readonly numValues: number
readonly isInnerParam: boolean
constructor(data: { field: TxnField; wtype: wtypes.WType; numValues?: number; isInnerParam?: boolean }) {
/**
* If field is an array, accept individual arguments and convert to an array
*/
readonly arrayPromote: boolean
constructor(data: { field: TxnField; wtype: wtypes.WType; numValues?: number; isInnerParam?: boolean; arrayPromote?: boolean }) {
this.immediate = data.field
this.wtype = data.wtype
this.numValues = data.numValues ?? 1
this.isInnerParam = data.isInnerParam ?? true
this.arrayPromote = data.arrayPromote ?? false
}
}

Expand Down Expand Up @@ -167,6 +172,16 @@ export const TxnFields: Record<TxnField, TxnFieldData> = {
// v5
Logs: new TxnFieldData({ field: TxnField.Logs, wtype: wtypes.bytesWType, numValues: 32, isInnerParam: false }),
// v7
ApprovalProgramPages: new TxnFieldData({ field: TxnField.ApprovalProgramPages, wtype: wtypes.bytesWType, numValues: 4 }),
ClearStateProgramPages: new TxnFieldData({ field: TxnField.ClearStateProgramPages, wtype: wtypes.bytesWType, numValues: 4 }),
ApprovalProgramPages: new TxnFieldData({
field: TxnField.ApprovalProgramPages,
wtype: wtypes.bytesWType,
numValues: 4,
arrayPromote: true,
}),
ClearStateProgramPages: new TxnFieldData({
field: TxnField.ClearStateProgramPages,
wtype: wtypes.bytesWType,
numValues: 4,
arrayPromote: true,
}),
}
2 changes: 1 addition & 1 deletion src/awst_build/ast-visitors/base-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
return this.context.getBuilderForNode(node.name)
}
const property = this.textVisitor.accept(node.name)
return target.memberAccess(property, this.sourceLocation(node))
return target.memberAccess(property, this.sourceLocation(node.name))
}

visitElementAccessExpression(node: ts.ElementAccessExpression): NodeBuilder {
Expand Down
37 changes: 36 additions & 1 deletion src/awst_build/eb/arc4/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import { nodeFactory } from '../../../awst/node-factory'
import type { BytesConstant, Expression } from '../../../awst/nodes'
import { EqualityComparison } from '../../../awst/nodes'
import type { SourceLocation } from '../../../awst/source-location'
import { wtypes } from '../../../awst/wtypes'
import { logger } from '../../../logger'
import { codeInvariant, hexToUint8Array } from '../../../util'
import { isArc4EncodableType, ptypeToArc4EncodedType } from '../../arc4-util'
import type { PType } from '../../ptypes'
import { bytesPType, stringPType } from '../../ptypes'
import { ARC4EncodedType, decodeArc4Function, encodeArc4Function, interpretAsArc4Function } from '../../ptypes/arc4-types'
import {
ARC4EncodedType,
decodeArc4Function,
encodeArc4Function,
interpretAsArc4Function,
methodSelectorFunction,
} from '../../ptypes/arc4-types'
import { instanceEb } from '../../type-registry'
import type { InstanceBuilder, NodeBuilder } from '../index'
import { FunctionBuilder } from '../index'
Expand Down Expand Up @@ -152,3 +159,31 @@ function getPrefixValue(arg: InstanceBuilder | undefined): BytesConstant | undef
logger.error(arg.sourceLocation, `Expected literal string: 'none' | 'log'`)
}
}

export class MethodSelectorFunctionBuilder extends FunctionBuilder {
readonly ptype = methodSelectorFunction

call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): NodeBuilder {
const {
args: [methodSignature],
} = parseFunctionArgs({
args,
typeArgs,
genericTypeArgs: 0,
callLocation: sourceLocation,
funcName: this.typeDescription,
argSpec: (a) => [a.required(stringPType)],
})

const signature = requireStringConstant(methodSignature).value

return instanceEb(
nodeFactory.methodConstant({
value: signature,
wtype: wtypes.bytesWType,
sourceLocation,
}),
bytesPType,
)
}
}
53 changes: 53 additions & 0 deletions src/awst_build/eb/compiled/compile-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ContractReference, LogicSigReference } from '../../../awst/models'
import { nodeFactory } from '../../../awst/node-factory'
import type { SourceLocation } from '../../../awst/source-location'
import { invariant } from '../../../util'
import type { PType } from '../../ptypes'
import { compiledContractType, compileFunctionType, ContractClassPType, LogicSigPType } from '../../ptypes'
import { instanceEb } from '../../type-registry'
import type { NodeBuilder } from '../index'
import { FunctionBuilder } from '../index'
import { parseFunctionArgs } from '../util/arg-parsing'

export class CompileFunctionBuilder extends FunctionBuilder {
readonly ptype = compileFunctionType

call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): NodeBuilder {
const {
args: [contractOrSig, options],
} = parseFunctionArgs({
args,
typeArgs,
genericTypeArgs: 0,
callLocation: sourceLocation,
funcName: this.typeDescription,
argSpec: (a) => [a.required(ContractClassPType, LogicSigPType), a.optional()],
})

if (contractOrSig.ptype instanceof ContractClassPType) {
return instanceEb(
nodeFactory.compiledContract({
contract: ContractReference.fromPType(contractOrSig.ptype),
allocationOverrides: new Map(),
prefix: null,
templateVariables: new Map(),
wtype: compiledContractType.wtype,
sourceLocation,
}),
compiledContractType,
)
} else {
invariant(contractOrSig.ptype instanceof LogicSigPType, 'ptype must be LogicSigPType')
return instanceEb(
nodeFactory.compiledLogicSig({
logicSig: LogicSigReference.fromPType(contractOrSig.ptype),
prefix: null,
templateVariables: new Map(),
wtype: compiledContractType.wtype,
sourceLocation,
}),
compiledContractType,
)
}
}
}
29 changes: 26 additions & 3 deletions src/awst_build/eb/contract-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import type { SourceLocation } from '../../awst/source-location'
import { wtypes } from '../../awst/wtypes'
import { Constants } from '../../constants'
import { CodeError } from '../../errors'
import { codeInvariant } from '../../util'
import { codeInvariant, invariant } from '../../util'
import type { AwstBuildContext } from '../context/awst-build-context'
import type { ContractClassPType, PType } from '../ptypes'
import { arc4BaseContractType, baseContractType, StorageProxyPType } from '../ptypes'
import type { PType } from '../ptypes'
import { arc4BaseContractType, baseContractType, ContractClassPType, StorageProxyPType } from '../ptypes'

import { instanceEb } from '../type-registry'

Expand Down Expand Up @@ -87,3 +87,26 @@ export class ContractSuperBuilder extends ContractThisBuilder {
return super.memberAccess(name, sourceLocation)
}
}

export class ContractClassBuilder extends InstanceBuilder {
resolve(): Expression {
throw new CodeError('Contract class cannot be used as a value')
}
resolveLValue(): LValue {
throw new CodeError('Contract class cannot be used as a value')
}
readonly ptype: ContractClassPType
constructor(sourceLocation: SourceLocation, ptype: PType) {
super(sourceLocation)
invariant(ptype instanceof ContractClassPType, 'ptype must be ContractClassPType')
this.ptype = ptype
}

newCall(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): InstanceBuilder {
throw new CodeError('Contract class cannot be constructed manually')
}

call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): InstanceBuilder {
throw new CodeError('Contract class cannot be called manually')
}
}
Loading

0 comments on commit 4137c7a

Please sign in to comment.