Skip to content

Commit

Permalink
Merge pull request #20 from algorandfoundation/feat-precompiled
Browse files Browse the repository at this point in the history
feat: implement stubs for compile functions
  • Loading branch information
boblat authored Dec 27, 2024
2 parents b61fd52 + 3ffe395 commit f08cd9f
Show file tree
Hide file tree
Showing 14 changed files with 420 additions and 15 deletions.
136 changes: 136 additions & 0 deletions examples/precompiled/contract.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { bytes, uint64 } from '@algorandfoundation/algorand-typescript'
import { assert, compile, Contract, itxn } from '@algorandfoundation/algorand-typescript'
import { decodeArc4, encodeArc4, methodSelector, OnCompleteAction } from '@algorandfoundation/algorand-typescript/arc4'
import { Hello, HelloTemplate, HelloTemplateCustomPrefix, LargeProgram, TerribleCustodialAccount } from './precompiled-apps.algo'

export class HelloFactory extends Contract {
test_compile_contract() {
const compiled = compile(Hello)

const helloApp = itxn
.applicationCall({
appArgs: [methodSelector('create(string)void'), encodeArc4('hello')],
approvalProgram: compiled.approvalProgram,
clearStateProgram: compiled.clearStateProgram,
globalNumBytes: 1,
})
.submit().createdApp

const txn = itxn
.applicationCall({
appArgs: [methodSelector('greet(string)string'), encodeArc4('world')],
appId: helloApp,
})
.submit()
const result = decodeArc4<string>(txn.lastLog, 'log')

assert(result === 'hello world')

itxn
.applicationCall({
appId: helloApp,
appArgs: [methodSelector('delete()void')],
onCompletion: OnCompleteAction.DeleteApplication,
})
.submit()
}

test_compile_contract_with_template() {
const compiled = compile(HelloTemplate, { templateVars: { GREETING: 'hey' } })

const helloApp = itxn
.applicationCall({
appArgs: [methodSelector('create()void')],
approvalProgram: compiled.approvalProgram,
clearStateProgram: compiled.clearStateProgram,
globalNumBytes: 1,
})
.submit().createdApp

const txn = itxn
.applicationCall({
appArgs: [methodSelector('greet(string)string'), encodeArc4('world')],
appId: helloApp,
})
.submit()
const result = decodeArc4<string>(txn.lastLog, 'log')

assert(result === 'hey world')

itxn
.applicationCall({
appId: helloApp,
appArgs: [methodSelector('delete()void')],
onCompletion: OnCompleteAction.DeleteApplication,
})
.submit()
}

test_compile_contract_with_template_and_custom_prefix() {
const compiled = compile(HelloTemplateCustomPrefix, { templateVars: { GREETING: 'bonjour' }, templateVarsPrefix: 'PRFX_' })

const helloApp = itxn
.applicationCall({
appArgs: [methodSelector('create()void')],
approvalProgram: compiled.approvalProgram,
clearStateProgram: compiled.clearStateProgram,
globalNumBytes: 1,
})
.submit().createdApp

const txn = itxn
.applicationCall({
appArgs: [methodSelector('greet(string)string'), encodeArc4('world')],
appId: helloApp,
})
.submit()
const result = decodeArc4<string>(txn.lastLog, 'log')

assert(result === 'bonjour world')

itxn
.applicationCall({
appId: helloApp,
appArgs: [methodSelector('delete()void')],
onCompletion: OnCompleteAction.DeleteApplication,
})
.submit()
}

test_compile_contract_large() {
const compiled = compile(LargeProgram)

const largeApp = itxn
.applicationCall({
approvalProgram: compiled.approvalProgram,
clearStateProgram: compiled.clearStateProgram,
extraProgramPages: compiled.extraProgramPages,
globalNumBytes: compiled.globalBytes,
})
.submit().createdApp

const txn = itxn
.applicationCall({
appArgs: [methodSelector('getBigBytesLength()uint64')],
appId: largeApp,
})
.submit()
const result = decodeArc4<uint64>(txn.lastLog, 'log')

assert(result === 4096)

itxn
.applicationCall({
appId: largeApp,
appArgs: [methodSelector('delete()void')],
onCompletion: OnCompleteAction.DeleteApplication,
})
.submit()
}

test_compile_logic_sig(account: bytes) {
const compiled = compile(TerribleCustodialAccount)

assert(compiled.account.bytes === account)
}
}
81 changes: 81 additions & 0 deletions examples/precompiled/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { arc4 } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { afterEach, describe, it } from 'vitest'
import { ABI_RETURN_VALUE_LOG_PREFIX, MAX_BYTES_SIZE } from '../../src/constants'
import { asUint64Cls } from '../../src/util'
import { HelloFactory } from './contract.algo'
import { Hello, HelloTemplate, HelloTemplateCustomPrefix, LargeProgram, TerribleCustodialAccount } from './precompiled-apps.algo'

describe('pre compiled app calls', () => {
const ctx = new TestExecutionContext()
afterEach(() => {
ctx.reset()
})

it('should be able to compile and call a precompiled app', () => {
// Arrange
const helloApp = ctx.any.application({
approvalProgram: ctx.any.bytes(20),
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(new arc4.Str('hello world').bytes)],
})
ctx.setCompiledApp(Hello, helloApp.id)

const contract = ctx.contract.create(HelloFactory)

// Act
contract.test_compile_contract()
})

it('should be able to compile with template vars and call a precompiled app', () => {
// Arrange
const helloTemplateApp = ctx.any.application({
approvalProgram: ctx.any.bytes(20),
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(new arc4.Str('hey world').bytes)],
})
ctx.setCompiledApp(HelloTemplate, helloTemplateApp.id)

const contract = ctx.contract.create(HelloFactory)

// Act
contract.test_compile_contract_with_template()
})

it('should be able to compile with template vars and custom prefix', () => {
// Arrange
const helloTemplateCustomPrefixApp = ctx.any.application({
approvalProgram: ctx.any.bytes(20),
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(new arc4.Str('bonjour world').bytes)],
})
ctx.setCompiledApp(HelloTemplateCustomPrefix, helloTemplateCustomPrefixApp.id)

const contract = ctx.contract.create(HelloFactory)

// Act
contract.test_compile_contract_with_template_and_custom_prefix()
})

it('should be able to compile large program', () => {
// Arrange
const largeProgramApp = ctx.any.application({
approvalProgram: ctx.any.bytes(20),
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(asUint64Cls(MAX_BYTES_SIZE).toBytes().asAlgoTs())],
})
ctx.setCompiledApp(LargeProgram, largeProgramApp.id)

const contract = ctx.contract.create(HelloFactory)

// Act
contract.test_compile_contract_large()
})

it('should be able to compile logic sig', () => {
// Arrange
const terribleCustodialAccount = ctx.any.account()
ctx.setCompiledLogicSig(TerribleCustodialAccount, terribleCustodialAccount)

const contract = ctx.contract.create(HelloFactory)

// Act
contract.test_compile_logic_sig(terribleCustodialAccount.bytes)
})
})
65 changes: 65 additions & 0 deletions examples/precompiled/precompiled-apps.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { abimethod, Contract, GlobalState, LogicSig, op, TemplateVar } from '@algorandfoundation/algorand-typescript'

abstract class HelloBase extends Contract {
greeting = GlobalState({ initialValue: '' })

@abimethod({ allowActions: 'DeleteApplication' })
delete() {}

@abimethod({ allowActions: 'UpdateApplication' })
update() {}

greet(name: string): string {
return `${this.greeting.value} ${name}`
}
}

export class Hello extends HelloBase {
@abimethod({ onCreate: 'require' })
create(greeting: string) {
this.greeting.value = greeting
}
}

export class HelloTemplate extends HelloBase {
constructor() {
super()
this.greeting.value = TemplateVar<string>('GREETING')
}

@abimethod({ onCreate: 'require' })
create() {}
}

export class HelloTemplateCustomPrefix extends HelloBase {
constructor() {
super()
this.greeting.value = TemplateVar<string>('GREETING', 'PRFX_')
}

@abimethod({ onCreate: 'require' })
create() {}
}

function getBigBytes() {
return op.bzero(4096)
}

export class LargeProgram extends Contract {
getBigBytesLength() {
return getBigBytes().length
}

@abimethod({ allowActions: 'DeleteApplication' })
delete() {}
}

/**
* This logic sig can be used to create a custodial account that will allow any transaction to transfer its
* funds/assets.
*/
export class TerribleCustodialAccount extends LogicSig {
program() {
return true
}
}
1 change: 1 addition & 0 deletions examples/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const config: RollupOptions = {
'examples/voting/contract.algo.ts',
'examples/simple-voting/contract.algo.ts',
'examples/zk-whitelist/contract.algo.ts',
'examples/precompiled/contract.algo.ts',
],
output: [
{
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"tslib": "^2.6.2"
},
"dependencies": {
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.23",
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.24",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.36",
"elliptic": "^6.5.7",
"js-sha256": "^0.11.0",
Expand Down
55 changes: 55 additions & 0 deletions src/impl/compiled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
Account,
BaseContract,
CompileContractOptions,
CompiledContract,
CompiledLogicSig,
CompileLogicSigOptions,
LogicSig,
} from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { ConstructorFor } from '../typescript-helpers'
import { ApplicationData } from './application'

export function compileImpl(
artefact: ConstructorFor<BaseContract> | ConstructorFor<LogicSig>,
options?: CompileContractOptions | CompileLogicSigOptions,
): CompiledLogicSig | CompiledContract {
let app: ApplicationData | undefined
let account: Account | undefined
const compiledApp = lazyContext.value.getCompiledApp(artefact as ConstructorFor<BaseContract>)
const compiledLogicSig = lazyContext.value.getCompiledLogicSig(artefact as ConstructorFor<LogicSig>)
if (compiledApp !== undefined) {
app = lazyContext.ledger.applicationDataMap.get(compiledApp[1])
}
if (compiledLogicSig !== undefined) {
account = compiledLogicSig[1]
}
if (options?.templateVars) {
Object.entries(options.templateVars).forEach(([key, value]) => {
lazyContext.value.setTemplateVar(key, value, options.templateVarsPrefix)
})
}
return new Proxy({} as CompiledLogicSig | CompiledContract, {
get: (_target, prop) => {
switch (prop) {
case 'approvalProgram':
return app?.application.approvalProgram ?? [lazyContext.any.bytes(10), lazyContext.any.bytes(10)]
case 'clearStateProgram':
return app?.application.clearStateProgram ?? [lazyContext.any.bytes(10), lazyContext.any.bytes(10)]
case 'extraProgramPages':
return (options as CompileContractOptions)?.extraProgramPages ?? app?.application.extraProgramPages ?? lazyContext.any.uint64()
case 'globalUints':
return (options as CompileContractOptions)?.globalUints ?? app?.application.globalNumUint ?? lazyContext.any.uint64()
case 'globalBytes':
return (options as CompileContractOptions)?.globalBytes ?? app?.application.globalNumBytes ?? lazyContext.any.uint64()
case 'localUints':
return (options as CompileContractOptions)?.localUints ?? app?.application.localNumUint ?? lazyContext.any.uint64()
case 'localBytes':
return (options as CompileContractOptions)?.localBytes ?? app?.application.localNumBytes ?? lazyContext.any.uint64()
case 'account':
return account ?? lazyContext.any.account()
}
},
})
}
Loading

0 comments on commit f08cd9f

Please sign in to comment.