Skip to content

Commit

Permalink
feat: implement stubs for LogicSig and arg, len op codes
Browse files Browse the repository at this point in the history
  • Loading branch information
boblat committed Dec 19, 2024
1 parent 0480033 commit 1589c95
Show file tree
Hide file tree
Showing 13 changed files with 1,416 additions and 187 deletions.
39 changes: 39 additions & 0 deletions examples/htlc-logicsig/signature.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Account, Bytes, Global, LogicSig, op, TransactionType, Txn, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'

export default class HashedTimeLockedLogicSig extends LogicSig {
program(): boolean | uint64 {
// Participants
const sellerAddress = Bytes(algosdk.decodeAddress('6ZHGHH5Z5CTPCF5WCESXMGRSVK7QJETR63M3NY5FJCUYDHO57VTCMJOBGY').publicKey)
const buyerAddress = Bytes(algosdk.decodeAddress('7Z5PWO2C6LFNQFGHWKSK5H47IQP5OJW2M3HA2QPXTY3WTNP5NU2MHBW27M').publicKey)
const seller = Account(sellerAddress)
const buyer = Account(buyerAddress)

// Contract parameters
const feeLimit = Uint64(1000)
const secretHash = Bytes.fromHex('2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b')
const timeout = Uint64(3000)

// Transaction conditions
const isPayment = Txn.typeEnum === TransactionType.Payment
const isFeeAcceptable = Txn.fee < feeLimit
const isNoCloseTo = Txn.closeRemainderTo === Global.zeroAddress
const isNoRekey = Txn.rekeyTo === Global.zeroAddress

// Safety conditions
const safetyConditions = isPayment && isNoCloseTo && isNoRekey

// Seller receives payment if correct secret is provided
const isToSeller = Txn.receiver === seller
const isSecretCorrect = op.sha256(op.arg(0)) === secretHash

Check failure on line 28 in examples/htlc-logicsig/signature.algo.ts

View workflow job for this annotation

GitHub Actions / Build @algorandfoundation/algorand-typescript-testing / node-ci

examples/htlc-logicsig/signature.spec.ts > HTLC LogicSig > seller receives payment if correct secret is provided

TypeError: op.arg is not a function ❯ HashedTimeLockedLogicSig.program examples/htlc-logicsig/signature.algo.ts:28:42 ❯ TestExecutionContext.executeLogicSig src/test-execution-context.ts:132:23 ❯ examples/htlc-logicsig/signature.spec.ts:28:28 ❯ Object.execute src/subcontexts/transaction-context.ts:102:24 ❯ examples/htlc-logicsig/signature.spec.ts:27:8
const sellerReceives = isToSeller && isSecretCorrect

// Buyer receives refund after timeout
const isToBuyer = Txn.receiver === buyer
const isAfterTimeout = Txn.firstValid > timeout
const buyerReceivesRefund = isToBuyer && isAfterTimeout

// Final contract logic
return isFeeAcceptable && safetyConditions && (sellerReceives || buyerReceivesRefund)
}
}
32 changes: 32 additions & 0 deletions examples/htlc-logicsig/signature.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Account, Bytes } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import algosdk from 'algosdk'
import { afterEach, describe, expect, it } from 'vitest'
import { ZERO_ADDRESS } from '../../src/constants'
import HashedTimeLockedLogicSig from './signature.algo'

describe('HTLC LogicSig', () => {
const ctx = new TestExecutionContext()

afterEach(() => {
ctx.reset()
})

it('seller receives payment if correct secret is provided', () => {
const receiverAddress = Bytes(algosdk.decodeAddress('6ZHGHH5Z5CTPCF5WCESXMGRSVK7QJETR63M3NY5FJCUYDHO57VTCMJOBGY').publicKey)
ctx.txn
.createScope([
ctx.any.txn.payment({
fee: 500,
firstValid: 1000,
closeRemainderTo: Account(ZERO_ADDRESS),
rekeyTo: Account(ZERO_ADDRESS),
receiver: Account(receiverAddress),
}),
])
.execute(() => {
const result = ctx.executeLogicSig(new HashedTimeLockedLogicSig(), Bytes('secret'))
expect(result).toBe(true)
})
})
})
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.

7 changes: 4 additions & 3 deletions src/impl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ export { AppLocal } from './app-local'
export { AppParams } from './app-params'
export { AssetHolding } from './asset-holding'
export { AssetParams } from './asset-params'
export { Block } from './block'
export { Box } from './box'
export * from './crypto'
export { Global } from './global'
export { GTxn } from './gtxn'
export { GITxn, ITxn, ITxnCreate } from './itxn'
export { arg } from './logicSigArg'
export * from './pure'
export { Scratch, gloadBytes, gloadUint64 } from './scratch'
export { Block } from './block'
export { Txn, gaid } from './txn'
export { gloadBytes, gloadUint64, Scratch } from './scratch'
export { gaid, Txn } from './txn'
8 changes: 8 additions & 0 deletions src/impl/logicSigArg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { bytes, internal } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { asNumber } from '../util'

export const arg = (a: internal.primitives.StubUint64Compat): bytes => {
const index = asNumber(a)
return lazyContext.value.activeLogicSigArgs[index]
}
6 changes: 5 additions & 1 deletion src/impl/pure.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Base64, biguint, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import { BITS_IN_BYTE, MAX_BYTES_SIZE, MAX_UINT64, MAX_UINT8, UINT64_SIZE } from '../constants'
import { notImplementedError, testInvariant } from '../errors'
import { asBigUint, asBytes, asMaybeBytesCls, asMaybeUint64Cls, asUint64Cls, binaryStringToBytes } from '../util'
import { asBigUint, asBytes, asBytesCls, asMaybeBytesCls, asMaybeUint64Cls, asUint64Cls, binaryStringToBytes } from '../util'

export const addw = (a: internal.primitives.StubUint64Compat, b: internal.primitives.StubUint64Compat): readonly [uint64, uint64] => {
const uint64A = internal.primitives.Uint64Cls.fromCompat(a)
Expand Down Expand Up @@ -172,6 +172,10 @@ export const itob = (a: internal.primitives.StubUint64Compat): bytes => {
return asUint64Cls(a).toBytes().asAlgoTs()
}

export const len = (a: internal.primitives.StubBytesCompat): uint64 => {
return asBytesCls(a).length.asAlgoTs()
}

export const mulw = (a: internal.primitives.StubUint64Compat, b: internal.primitives.StubUint64Compat): readonly [uint64, uint64] => {
const uint64A = internal.primitives.Uint64Cls.fromCompat(a)
const uint64B = internal.primitives.Uint64Cls.fromCompat(b)
Expand Down
13 changes: 13 additions & 0 deletions src/runtime-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { internal } from '@algorandfoundation/algorand-typescript'
import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
import { MAX_UINT64 } from './constants'
import type { TypeInfo } from './encoders'
import { AccountCls } from './impl/account'
import { DeliberateAny } from './typescript-helpers'
import { nameOfType } from './util'

Expand Down Expand Up @@ -37,6 +38,9 @@ export function binaryOp(left: unknown, right: unknown, op: BinaryOps) {
if (left instanceof ARC4Encoded && right instanceof ARC4Encoded) {
return arc4EncodedOp(left, right, op)
}
if (left instanceof AccountCls && right instanceof AccountCls) {
return accountBinaryOp(left, right, op)
}
if (left instanceof internal.primitives.BigUintCls || right instanceof internal.primitives.BigUintCls) {
return bigUintBinaryOp(left, right, op)
}
Expand Down Expand Up @@ -84,6 +88,15 @@ function arc4EncodedOp(left: ARC4Encoded, right: ARC4Encoded, op: BinaryOps): De
internal.errors.internalError(`Unsupported operator ${op}`)
}
}

function accountBinaryOp(left: AccountCls, right: AccountCls, op: BinaryOps): DeliberateAny {
switch (op) {
case '===':
return bytesBinaryOp(left.bytes, right.bytes, op)
default:
internal.errors.internalError(`Unsupported operator ${op}`)
}
}
function uint64BinaryOp(left: DeliberateAny, right: DeliberateAny, op: BinaryOps): DeliberateAny {
const lbi = internal.primitives.Uint64Cls.fromCompat(left).valueOf()
const rbi = internal.primitives.Uint64Cls.fromCompat(right).valueOf()
Expand Down
17 changes: 16 additions & 1 deletion src/test-execution-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Account, Application, Asset, Bytes, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { Account, Application, Asset, Bytes, bytes, internal, LogicSig, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import { captureMethodConfig } from './abi-metadata'
import { DecodedLogs, LogDecoding } from './decode-logs'
Expand Down Expand Up @@ -27,6 +27,7 @@ export class TestExecutionContext implements internal.ExecutionContext {
#txnContext: TransactionContext
#valueGenerator: ValueGenerator
#defaultSender: Account
#activeLogicSigArgs: bytes[]

constructor(defaultSenderAddress?: bytes) {
internal.ctxMgr.instance = this
Expand All @@ -35,6 +36,7 @@ export class TestExecutionContext implements internal.ExecutionContext {
this.#txnContext = new TransactionContext()
this.#valueGenerator = new ValueGenerator()
this.#defaultSender = Account(defaultSenderAddress ?? Bytes.fromBase32(algosdk.generateAccount().addr))
this.#activeLogicSigArgs = []
}

account(address?: bytes): Account {
Expand Down Expand Up @@ -120,6 +122,19 @@ export class TestExecutionContext implements internal.ExecutionContext {
}
}

get activeLogicSigArgs(): bytes[] {
return this.#activeLogicSigArgs
}

executeLogicSig(logicSig: LogicSig, ...args: bytes[]): boolean | uint64 {
this.#activeLogicSigArgs = args
try {
return logicSig.program()
} finally {
this.#activeLogicSigArgs = []
}
}

reset() {
this.#contractContext = new ContractContext()
this.#ledgerContext = new LedgerContext()
Expand Down
7 changes: 7 additions & 0 deletions tests/artifacts/miscellaneous-ops/contract.algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ export class MiscellaneousOpsContract extends arc4.Contract {
return result
}

@arc4.abimethod()
public verify_bytes_len(a: bytes, pad_a_size: uint64): uint64 {
const paddedA = op.bzero(pad_a_size).concat(a)
const result = op.len(paddedA)
return result
}

@arc4.abimethod()
public verify_mulw(a: uint64, b: uint64): readonly [uint64, uint64] {
const result = op.mulw(a, b)
Expand Down
Loading

0 comments on commit 1589c95

Please sign in to comment.