Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions examples/scratch-storage/contract.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { arc4, assert, BaseContract, Bytes, op, uint64, Uint64 } from '@algorandfoundation/algorand-typescript'

export class ScratchSlotsContract extends arc4.Contract {
@arc4.abimethod()
public storeData(): boolean {
op.Scratch.store(1, Uint64(5))
op.Scratch.store(2, Bytes('Hello World'))
assert(op.Scratch.loadUint64(1) === Uint64(5))
assert(op.Scratch.loadBytes(2) === Bytes('Hello World'))
return true
}
}

export class SimpleScratchSlotsContract extends BaseContract {
approvalProgram(): boolean | uint64 {
assert(op.Scratch.loadUint64(1) === Uint64(5))
assert(op.Scratch.loadBytes(2) === Bytes('Hello World'))
return Uint64(1)
}
clearStateProgram(): boolean | uint64 {
return Uint64(1)
}
}
29 changes: 29 additions & 0 deletions examples/scratch-storage/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { afterEach, describe, expect, it } from 'vitest'
import { ScratchSlotsContract, SimpleScratchSlotsContract } from './contract.algo'
import { Bytes, Uint64 } from '@algorandfoundation/algorand-typescript'

describe('ScratchSlotsContract', () => {
const ctx = new TestExecutionContext()
afterEach(() => {
ctx.reset()
})
it('should be able to store data', async () => {
const contract = ctx.contract.create(ScratchSlotsContract)
const result = contract.storeData()
expect(result).toBe(true)

const scratchSpace = ctx.txn.lastGroup.getScratchSpace()

expect(scratchSpace[1]).toEqual(Uint64(5))
expect(scratchSpace[2]).toEqual(Bytes('Hello World'))
})

it('should be able to load stored data', async () => {
const contract = ctx.contract.create(SimpleScratchSlotsContract)
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(0), Uint64(5), Bytes('Hello World')] })]).execute(() => {
const result = contract.approvalProgram()
expect(result).toEqual(Uint64(1))
})
})
})
1 change: 1 addition & 0 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"postinstall": "patch-package",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --fix",
"lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" \"examples/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" \"examples/**/*.ts\" --fix",
"build": "run-s build:*",
"build:0-clean": "rimraf dist coverage",
"build:1-lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --max-warnings 0",
Expand Down
13 changes: 13 additions & 0 deletions src/impl/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { itob } from './pure'
import { asUint64 } from '../util'

export const Block: internal.opTypes.BlockType = {
blkSeed: function (a: internal.primitives.StubUint64Compat): bytes {
return itob(lazyContext.ledger.getBlockContent(a).seed)
},
blkTimestamp: function (a: internal.primitives.StubUint64Compat): uint64 {
return asUint64(lazyContext.ledger.getBlockContent(a).timestamp)
},
}
3 changes: 2 additions & 1 deletion src/impl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export { GTxn } from './gtxn'
export * from './pure'
export { Txn, gaid } from './txn'
export { GITxn, ITxn, ITxnCreate } from './itxn'
export { Scratch } from './scratch'
export { Scratch, gloadBytes, gloadUint64 } from './scratch'
export { Block } from './block'
28 changes: 26 additions & 2 deletions src/impl/scratch.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import { bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'

export const gloadUint64: internal.opTypes.GloadUint64Type = (
a: internal.primitives.StubUint64Compat,
b: internal.primitives.StubUint64Compat,
): uint64 => {
const txn = lazyContext.activeGroup.getTransaction(a)
const result = txn.getScratchSlot(b)
if (result instanceof internal.primitives.Uint64Cls) {
return result.asAlgoTs()
}
throw new internal.errors.InternalError('invalid scratch slot type')
}

export const gloadBytes: internal.opTypes.GloadBytesType = (
a: internal.primitives.StubUint64Compat,
b: internal.primitives.StubUint64Compat,
): bytes => {
const txn = lazyContext.activeGroup.getTransaction(a)
const result = txn.getScratchSlot(b)
if (result instanceof internal.primitives.BytesCls) {
return result.asAlgoTs()
}
throw new internal.errors.InternalError('invalid scratch slot type')
}

export const Scratch: internal.opTypes.ScratchType = {
loadBytes: function (a: internal.primitives.StubUint64Compat): bytes {
const result = lazyContext.activeGroup.activeTransaction.getScratchSlot(a)
if (result instanceof internal.primitives.BytesCls) {
return result as bytes
}
throw new internal.errors.InternalError('Invalid scratch slot type')
throw new internal.errors.InternalError('invalid scratch slot type')
},
loadUint64: function (a: internal.primitives.StubUint64Compat): uint64 {
const result = lazyContext.activeGroup.activeTransaction.getScratchSlot(a)
if (result instanceof internal.primitives.Uint64Cls) {
return result as uint64
}
throw new internal.errors.InternalError('Invalid scratch slot type')
throw new internal.errors.InternalError('invalid scratch slot type')
},
store: function (
a: internal.primitives.StubUint64Compat,
Expand Down
4 changes: 2 additions & 2 deletions src/impl/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export type ApplicationTransactionFields = TxnFields<gtxn.ApplicationTxn> &
approvalProgramPages: Array<bytes>
clearStateProgramPages: Array<bytes>
appLogs: Array<bytes>
scratchSpace: Array<bytes | uint64>
scratchSpace: Record<number, bytes | uint64>
}>

export class ApplicationTransaction extends TransactionBase implements gtxn.ApplicationTxn {
Expand Down Expand Up @@ -266,7 +266,7 @@ export class ApplicationTransaction extends TransactionBase implements gtxn.Appl
this.#apps = fields.apps ?? []
this.#approvalProgramPages = fields.approvalProgramPages ?? (fields.approvalProgram ? [fields.approvalProgram] : [])
this.#clearStateProgramPages = fields.clearStateProgramPages ?? (fields.clearStateProgram ? [fields.clearStateProgram] : [])
fields.scratchSpace?.forEach((v, i) => this.setScratchSlot(i, v))
Object.entries(fields.scratchSpace ?? {}).forEach(([k, v]) => this.setScratchSlot(Number(k), v))
}

readonly appId: Application
Expand Down
11 changes: 1 addition & 10 deletions src/impl/txn.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { Account, Application, arc4, Asset, bytes, internal, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { asNumber, asUint64, asUint64Cls } from '../util'
// import {
// getApplicationTransaction,
// getAssetConfigTransaction,
// getAssetFreezeTransaction,
// getAssetTransferTransaction,
// getKeyRegistrationTransaction,
// getPaymentTransaction,
// getTransaction,
// } from './gtxn'

export const gaid = (a: internal.primitives.StubUint64Compat): uint64 => {
const group = lazyContext.activeGroup
const transaction = group.transactions[asNumber(a)]
const transaction = group.getTransaction(a)
if (transaction.type === TransactionType.ApplicationCall) {
return transaction.createdApp.id
} else if (transaction.type === TransactionType.AssetConfig) {
Expand Down
26 changes: 26 additions & 0 deletions src/subcontexts/ledger-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import { AssetData } from '../impl/asset'
import { GlobalData } from '../impl/global'
import { asBigInt, asMaybeUint64Cls, asUint64, asUint64Cls, iterBigInt } from '../util'

interface BlockData {
seed: bigint
timestamp: bigint
}

export class LedgerContext {
appIdIter = iterBigInt(1001n, MAX_UINT64)
assetIdIter = iterBigInt(1001n, MAX_UINT64)
applicationDataMap = new Map<bigint, ApplicationData>()
appIdContractMap = new Map<bigint, BaseContract>()
accountDataMap = new AccountMap<AccountData>()
assetDataMap = new Map<bigint, AssetData>()
blocks = new Map<bigint, BlockData>()
globalData = new GlobalData()

addAppIdContractMap(appId: internal.primitives.StubUint64Compat, contract: BaseContract): void {
Expand Down Expand Up @@ -76,4 +82,24 @@ export class LedgerContext {
...data,
}
}

setBlock(
index: internal.primitives.StubUint64Compat,
seed: internal.primitives.StubUint64Compat,
timestamp: internal.primitives.StubUint64Compat,
): void {
const i = asBigInt(index)
const s = asBigInt(seed)
const t = asBigInt(timestamp)

this.blocks.set(i, { seed: s, timestamp: t })
}

getBlockContent(index: internal.primitives.StubUint64Compat): BlockData {
const i = asBigInt(index)
if (this.blocks.has(i)) {
return this.blocks.get(i)!
}
throw internal.errors.internalError(`Block ${i} not set`)
}
}
4 changes: 4 additions & 0 deletions src/subcontexts/transaction-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ export class TransactionGroup {
return this.constructingItxnGroup.at(-1)!
}

getScratchSpace() {
return this.activeTransaction.scratchSpace
}

getScratchSlot(index: internal.primitives.StubUint64Compat): bytes | uint64 {
return this.activeTransaction.getScratchSlot(index)
}
Expand Down
67 changes: 63 additions & 4 deletions tests/state-op-codes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
INITIAL_BALANCE_MICRO_ALGOS,
} from './avm-invoker'
import { asUint8Array } from './util'
import { Block, gloadBytes, gloadUint64 } from '../src/impl'

describe('State op codes', async () => {
const ctx = new TestExecutionContext()
Expand Down Expand Up @@ -366,14 +367,72 @@ describe('State op codes', async () => {
[3, Uint64(42)],
[255, Bytes('max_index')],
])('should return the correct field value of the scratch slot', async (index: number, value: bytes | uint64) => {
const newScratchSpace = Array(256).fill(Uint64(0))
newScratchSpace[index] = value

ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: newScratchSpace })]).execute(() => {})
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: { [index]: value } })]).execute(() => {})

expect(ctx.txn.lastGroup.getScratchSlot(index)).toEqual(value)

expect(() => ctx.txn.lastGroup.getScratchSlot(256)).toThrow('invalid scratch slot')
})
})

describe('gloadBytes', async () => {
it('should return the correct field value of the scratch slot', async () => {
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(0), Bytes('hello'), Bytes('world')] })]).execute(() => {
const slot1 = gloadBytes(0, 1)
const slot2 = gloadBytes(0, 2)
expect(slot1).toStrictEqual('hello')
expect(slot2).toStrictEqual('world')
})
})
it('should throw error if the scratch slot is not a bytes type', async () => {
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(0), Bytes('hello'), Bytes('world')] })]).execute(() => {
expect(() => gloadBytes(0, 0)).toThrow('invalid scratch slot type')
})
})
it('should throw error if the scratch slot is out of range', async () => {
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(0), Bytes('hello'), Bytes('world')] })]).execute(() => {
expect(() => gloadBytes(0, 256)).toThrow('invalid scratch slot')
})
})
})

describe('gloadUint64', async () => {
it('should return the correct field value of the scratch slot', async () => {
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(7), Uint64(42), Bytes('world')] })]).execute(() => {
const slot0 = gloadUint64(0, 0)
const slot1 = gloadUint64(0, 1)
expect(slot0).toStrictEqual(7n)
expect(slot1).toStrictEqual(42n)
})
})
it('should throw error if the scratch slot is not a uint64 type', async () => {
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(7), Uint64(42), Bytes('world')] })]).execute(() => {
expect(() => gloadUint64(0, 2)).toThrow('invalid scratch slot type')
})
})
it('should throw error if the scratch slot is out of range', async () => {
ctx.txn.createScope([ctx.any.txn.applicationCall({ scratchSpace: [Uint64(7), Uint64(42), Bytes('world')] })]).execute(() => {
expect(() => gloadUint64(0, 256)).toThrow('invalid scratch slot')
})
})
})

describe('Block', async () => {
it('should return the correct field value of the block', async () => {
const index = 42
const seed = 123
const timestamp = 1234567890
ctx.ledger.setBlock(index, seed, timestamp)
const seedResult = op.btoi(Block.blkSeed(Uint64(index)))
const timestampResult = Block.blkTimestamp(Uint64(index))

expect(seedResult).toEqual(Uint64(seed))
expect(timestampResult).toEqual(Uint64(timestamp))
})
it('should throw error if the block is not set', async () => {
const index = 42
expect(() => Block.blkSeed(Uint64(index))).toThrow('Block 42 not set')
expect(() => Block.blkTimestamp(Uint64(index))).toThrow('Block 42 not set')
})
})
})