Skip to content

Commit 2084c95

Browse files
committed
feat: implement stubs for LogicSig and arg, len op codes
1 parent ed4fd56 commit 2084c95

File tree

13 files changed

+1416
-187
lines changed

13 files changed

+1416
-187
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Account, Bytes, Global, LogicSig, op, TransactionType, Txn, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import algosdk from 'algosdk'
3+
4+
export default class HashedTimeLockedLogicSig extends LogicSig {
5+
program(): boolean | uint64 {
6+
// Participants
7+
const sellerAddress = Bytes(algosdk.decodeAddress('6ZHGHH5Z5CTPCF5WCESXMGRSVK7QJETR63M3NY5FJCUYDHO57VTCMJOBGY').publicKey)
8+
const buyerAddress = Bytes(algosdk.decodeAddress('7Z5PWO2C6LFNQFGHWKSK5H47IQP5OJW2M3HA2QPXTY3WTNP5NU2MHBW27M').publicKey)
9+
const seller = Account(sellerAddress)
10+
const buyer = Account(buyerAddress)
11+
12+
// Contract parameters
13+
const feeLimit = Uint64(1000)
14+
const secretHash = Bytes.fromHex('2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b')
15+
const timeout = Uint64(3000)
16+
17+
// Transaction conditions
18+
const isPayment = Txn.typeEnum === TransactionType.Payment
19+
const isFeeAcceptable = Txn.fee < feeLimit
20+
const isNoCloseTo = Txn.closeRemainderTo === Global.zeroAddress
21+
const isNoRekey = Txn.rekeyTo === Global.zeroAddress
22+
23+
// Safety conditions
24+
const safetyConditions = isPayment && isNoCloseTo && isNoRekey
25+
26+
// Seller receives payment if correct secret is provided
27+
const isToSeller = Txn.receiver === seller
28+
const isSecretCorrect = op.sha256(op.arg(0)) === secretHash
29+
const sellerReceives = isToSeller && isSecretCorrect
30+
31+
// Buyer receives refund after timeout
32+
const isToBuyer = Txn.receiver === buyer
33+
const isAfterTimeout = Txn.firstValid > timeout
34+
const buyerReceivesRefund = isToBuyer && isAfterTimeout
35+
36+
// Final contract logic
37+
return isFeeAcceptable && safetyConditions && (sellerReceives || buyerReceivesRefund)
38+
}
39+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Account, Bytes } from '@algorandfoundation/algorand-typescript'
2+
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
3+
import algosdk from 'algosdk'
4+
import { afterEach, describe, expect, it } from 'vitest'
5+
import { ZERO_ADDRESS } from '../../src/constants'
6+
import HashedTimeLockedLogicSig from './signature.algo'
7+
8+
describe('HTLC LogicSig', () => {
9+
const ctx = new TestExecutionContext()
10+
11+
afterEach(() => {
12+
ctx.reset()
13+
})
14+
15+
it('seller receives payment if correct secret is provided', () => {
16+
const receiverAddress = Bytes(algosdk.decodeAddress('6ZHGHH5Z5CTPCF5WCESXMGRSVK7QJETR63M3NY5FJCUYDHO57VTCMJOBGY').publicKey)
17+
ctx.txn
18+
.createScope([
19+
ctx.any.txn.payment({
20+
fee: 500,
21+
firstValid: 1000,
22+
closeRemainderTo: Account(ZERO_ADDRESS),
23+
rekeyTo: Account(ZERO_ADDRESS),
24+
receiver: Account(receiverAddress),
25+
}),
26+
])
27+
.execute(() => {
28+
const result = ctx.executeLogicSig(new HashedTimeLockedLogicSig(), Bytes('secret'))
29+
expect(result).toBe(true)
30+
})
31+
})
32+
})

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/impl/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ export { AppLocal } from './app-local'
44
export { AppParams } from './app-params'
55
export { AssetHolding } from './asset-holding'
66
export { AssetParams } from './asset-params'
7+
export { Block } from './block'
78
export { Box } from './box'
89
export * from './crypto'
910
export { Global } from './global'
1011
export { GTxn } from './gtxn'
1112
export { GITxn, ITxn, ITxnCreate } from './itxn'
13+
export { arg } from './logicSigArg'
1214
export * from './pure'
13-
export { Scratch, gloadBytes, gloadUint64 } from './scratch'
14-
export { Block } from './block'
15-
export { Txn, gaid } from './txn'
15+
export { gloadBytes, gloadUint64, Scratch } from './scratch'
16+
export { gaid, Txn } from './txn'

src/impl/logicSigArg.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { bytes, internal } from '@algorandfoundation/algorand-typescript'
2+
import { lazyContext } from '../context-helpers/internal-context'
3+
import { asNumber } from '../util'
4+
5+
export const arg = (a: internal.primitives.StubUint64Compat): bytes => {
6+
const index = asNumber(a)
7+
return lazyContext.value.activeLogicSigArgs[index]
8+
}

src/impl/pure.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Base64, biguint, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { BITS_IN_BYTE, MAX_BYTES_SIZE, MAX_UINT64, MAX_UINT8, UINT64_SIZE } from '../constants'
33
import { notImplementedError, testInvariant } from '../errors'
4-
import { asBigUint, asBytes, asMaybeBytesCls, asMaybeUint64Cls, asUint64Cls, binaryStringToBytes } from '../util'
4+
import { asBigUint, asBytes, asBytesCls, asMaybeBytesCls, asMaybeUint64Cls, asUint64Cls, binaryStringToBytes } from '../util'
55

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

175+
export const len = (a: internal.primitives.StubBytesCompat): uint64 => {
176+
return asBytesCls(a).length.asAlgoTs()
177+
}
178+
175179
export const mulw = (a: internal.primitives.StubUint64Compat, b: internal.primitives.StubUint64Compat): readonly [uint64, uint64] => {
176180
const uint64A = internal.primitives.Uint64Cls.fromCompat(a)
177181
const uint64B = internal.primitives.Uint64Cls.fromCompat(b)

src/runtime-helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { internal } from '@algorandfoundation/algorand-typescript'
22
import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
33
import { MAX_UINT64 } from './constants'
44
import type { TypeInfo } from './encoders'
5+
import { AccountCls } from './impl/account'
56
import { DeliberateAny } from './typescript-helpers'
67
import { nameOfType } from './util'
78

@@ -37,6 +38,9 @@ export function binaryOp(left: unknown, right: unknown, op: BinaryOps) {
3738
if (left instanceof ARC4Encoded && right instanceof ARC4Encoded) {
3839
return arc4EncodedOp(left, right, op)
3940
}
41+
if (left instanceof AccountCls && right instanceof AccountCls) {
42+
return accountBinaryOp(left, right, op)
43+
}
4044
if (left instanceof internal.primitives.BigUintCls || right instanceof internal.primitives.BigUintCls) {
4145
return bigUintBinaryOp(left, right, op)
4246
}
@@ -84,6 +88,15 @@ function arc4EncodedOp(left: ARC4Encoded, right: ARC4Encoded, op: BinaryOps): De
8488
internal.errors.internalError(`Unsupported operator ${op}`)
8589
}
8690
}
91+
92+
function accountBinaryOp(left: AccountCls, right: AccountCls, op: BinaryOps): DeliberateAny {
93+
switch (op) {
94+
case '===':
95+
return bytesBinaryOp(left.bytes, right.bytes, op)
96+
default:
97+
internal.errors.internalError(`Unsupported operator ${op}`)
98+
}
99+
}
87100
function uint64BinaryOp(left: DeliberateAny, right: DeliberateAny, op: BinaryOps): DeliberateAny {
88101
const lbi = internal.primitives.Uint64Cls.fromCompat(left).valueOf()
89102
const rbi = internal.primitives.Uint64Cls.fromCompat(right).valueOf()

src/test-execution-context.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Account, Application, Asset, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, Asset, Bytes, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { captureMethodConfig } from './abi-metadata'
33
import { DecodedLogs, LogDecoding } from './decode-logs'
44
import * as ops from './impl'
@@ -27,6 +27,7 @@ export class TestExecutionContext implements internal.ExecutionContext {
2727
#txnContext: TransactionContext
2828
#valueGenerator: ValueGenerator
2929
#defaultSender: Account
30+
#activeLogicSigArgs: bytes[]
3031

3132
constructor(defaultSenderAddress?: bytes) {
3233
internal.ctxMgr.instance = this
@@ -35,6 +36,7 @@ export class TestExecutionContext implements internal.ExecutionContext {
3536
this.#txnContext = new TransactionContext()
3637
this.#valueGenerator = new ValueGenerator()
3738
this.#defaultSender = Account(defaultSenderAddress ?? getRandomBytes(32).asAlgoTs())
39+
this.#activeLogicSigArgs = []
3840
}
3941

4042
account(address?: bytes): Account {
@@ -120,6 +122,19 @@ export class TestExecutionContext implements internal.ExecutionContext {
120122
}
121123
}
122124

125+
get activeLogicSigArgs(): bytes[] {
126+
return this.#activeLogicSigArgs
127+
}
128+
129+
executeLogicSig(logicSig: LogicSig, ...args: bytes[]): boolean | uint64 {
130+
this.#activeLogicSigArgs = args
131+
try {
132+
return logicSig.program()
133+
} finally {
134+
this.#activeLogicSigArgs = []
135+
}
136+
}
137+
123138
reset() {
124139
this.#contractContext = new ContractContext()
125140
this.#ledgerContext = new LedgerContext()

tests/artifacts/miscellaneous-ops/contract.algo.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ export class MiscellaneousOpsContract extends arc4.Contract {
144144
return result
145145
}
146146

147+
@arc4.abimethod()
148+
public verify_bytes_len(a: bytes, pad_a_size: uint64): uint64 {
149+
const paddedA = op.bzero(pad_a_size).concat(a)
150+
const result = op.len(paddedA)
151+
return result
152+
}
153+
147154
@arc4.abimethod()
148155
public verify_mulw(a: uint64, b: uint64): readonly [uint64, uint64] {
149156
const result = op.mulw(a, b)

0 commit comments

Comments
 (0)