Skip to content

Commit

Permalink
Merge pull request #17 from algorandfoundation/refactor-sdk-utils
Browse files Browse the repository at this point in the history
refactor: remove dependency on algosdk, move algokit-utils to dev
  • Loading branch information
boblat authored Dec 20, 2024
2 parents 0480033 + cbbc86a commit ed4fd56
Show file tree
Hide file tree
Showing 28 changed files with 1,684 additions and 771 deletions.
1,366 changes: 868 additions & 498 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"build-examples": "rollup --config examples/rollup.config.ts --configPlugin typescript"
},
"devDependencies": {
"@algorandfoundation/algokit-utils": "^8.0.3",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@eslint/eslintrc": "3.1.0",
Expand Down Expand Up @@ -62,10 +63,8 @@
"tslib": "^2.6.2"
},
"dependencies": {
"@algorandfoundation/algokit-utils": "^6.2.1",
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.22",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.34",
"algosdk": "^2.9.0",
"elliptic": "^6.5.7",
"js-sha256": "^0.11.0",
"js-sha3": "^0.9.3",
Expand Down
10 changes: 7 additions & 3 deletions src/abi-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseContract, Contract } from '@algorandfoundation/algorand-typescript'
import { AbiMethodConfig, BareMethodConfig, CreateOptions, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript/arc4'
import { ABIMethod } from 'algosdk'
import { sha512_256 as js_sha512_256 } from 'js-sha512'
import { TypeInfo } from './encoders'
import { getArc4TypeName as getArc4TypeNameForARC4Encoded } from './impl/encoded-types'
import { DeliberateAny } from './typescript-helpers'
Expand Down Expand Up @@ -73,12 +73,16 @@ export const getArc4Signature = (metadata: AbiMetadata): string => {
if (metadata.methodSignature === undefined) {
const argTypes = metadata.argTypes.map((t) => JSON.parse(t) as TypeInfo).map(getArc4TypeName)
const returnType = getArc4TypeName(JSON.parse(metadata.returnType) as TypeInfo)
const method = new ABIMethod({ name: metadata.methodName, args: argTypes.map((t) => ({ type: t })), returns: { type: returnType } })
metadata.methodSignature = method.getSignature()
metadata.methodSignature = `${metadata.methodName}(${argTypes.join(',')})${returnType}`
}
return metadata.methodSignature
}

export const getArc4Selector = (metadata: AbiMetadata): Uint8Array => {
const hash = js_sha512_256.array(getArc4Signature(metadata))
return new Uint8Array(hash.slice(0, 4))
}

const getArc4TypeName = (t: TypeInfo): string => {
const map: Record<string, string | ((t: TypeInfo) => string)> = {
void: 'void',
Expand Down
19 changes: 19 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,22 @@ export const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75')

export const UINT64_OVERFLOW_UNDERFLOW_MESSAGE = 'Uint64 overflow or underflow'
export const BIGUINT_OVERFLOW_UNDERFLOW_MESSAGE = 'BigUint overflow or underflow'

export const APP_ID_PREFIX = 'appID'
export const HASH_BYTES_LENGTH = 32
export const ALGORAND_ADDRESS_BYTE_LENGTH = 36
export const ALGORAND_CHECKSUM_BYTE_LENGTH = 4
export const ALGORAND_ADDRESS_LENGTH = 58

export const PROGRAM_TAG = 'Program'

export const TRANSACTION_GROUP_MAX_SIZE = 16

export enum OnApplicationComplete {
NoOpOC = 0,
OptInOC = 1,
CloseOutOC = 2,
ClearStateOC = 3,
UpdateApplicationOC = 4,
DeleteApplicationOC = 5,
}
8 changes: 3 additions & 5 deletions src/impl/application.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Account, Application, Bytes, bytes, LocalState, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import { Account, Application, bytes, LocalState, uint64 } from '@algorandfoundation/algorand-typescript'
import { BytesMap } from '../collections/custom-key-map'
import { ALWAYS_APPROVE_TEAL_PROGRAM } from '../constants'
import { lazyContext } from '../context-helpers/internal-context'
import { Mutable } from '../typescript-helpers'
import { asBigInt, asUint64 } from '../util'
import { asUint64, getApplicationAddress } from '../util'
import { Uint64BackedCls } from './base'
import { GlobalStateCls } from './state'

Expand Down Expand Up @@ -76,7 +75,6 @@ export class ApplicationCls extends Uint64BackedCls implements Application {
return this.data.application.creator
}
get address(): Account {
const addr = algosdk.getApplicationAddress(asBigInt(this.id))
return Account(Bytes.fromBase32(addr))
return getApplicationAddress(this.id)
}
}
11 changes: 5 additions & 6 deletions src/impl/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { arc4, bytes, Bytes, Ecdsa, gtxn, internal, VrfVerify } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import { ec } from 'elliptic'
import { sha256 as js_sha256 } from 'js-sha256'
import { keccak256 as js_keccak256, sha3_256 as js_sha3_256 } from 'js-sha3'
import { sha512_256 as js_sha512_256 } from 'js-sha512'
import nacl from 'tweetnacl'
import { LOGIC_DATA_PREFIX } from '../constants'
import { LOGIC_DATA_PREFIX, PROGRAM_TAG } from '../constants'
import { lazyContext } from '../context-helpers/internal-context'
import { notImplementedError } from '../errors'
import { asBytes, asBytesCls } from '../util'
import { asBytes, asBytesCls, asUint8Array, conactUint8Arrays } from '../util'

export const sha256 = (a: internal.primitives.StubBytesCompat): bytes => {
const bytesA = internal.primitives.BytesCls.fromCompat(a)
Expand Down Expand Up @@ -59,10 +58,10 @@ export const ed25519verify = (
txn.onCompletion == arc4.OnCompleteAction[arc4.OnCompleteAction.ClearState] ? txn.clearStateProgram : txn.approvalProgram,
)

const logicSig = new algosdk.LogicSig(programBytes.asUint8Array())
const decodedAddress = algosdk.decodeAddress(logicSig.address())
const logicSig = conactUint8Arrays(asUint8Array(PROGRAM_TAG), programBytes.asUint8Array())
const logicSigAddress = js_sha512_256.array(logicSig)

const addressBytes = Bytes(decodedAddress.publicKey)
const addressBytes = Bytes(logicSigAddress)
const data = LOGIC_DATA_PREFIX.concat(addressBytes).concat(asBytes(a))
return ed25519verifyBare(data, b, c)
}
Expand Down
11 changes: 8 additions & 3 deletions src/impl/encoded-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ import {
UintN,
} from '@algorandfoundation/algorand-typescript/arc4'
import { encodingUtil } from '@algorandfoundation/puya-ts'
import { decodeAddress } from 'algosdk'
import assert from 'assert'
import { ABI_RETURN_VALUE_LOG_PREFIX, BITS_IN_BYTE, UINT64_SIZE } from '../constants'
import {
ABI_RETURN_VALUE_LOG_PREFIX,
ALGORAND_ADDRESS_BYTE_LENGTH,
ALGORAND_CHECKSUM_BYTE_LENGTH,
BITS_IN_BYTE,
UINT64_SIZE,
} from '../constants'
import { fromBytes, TypeInfo } from '../encoders'
import { DeliberateAny } from '../typescript-helpers'
import { asBigUint, asBigUintCls, asBytesCls, asUint64, asUint8Array, conactUint8Arrays, uint8ArrayToNumber } from '../util'
Expand Down Expand Up @@ -429,7 +434,7 @@ export class AddressImpl extends Address {
if (value === undefined) {
uint8ArrayValue = new Uint8Array(32)
} else if (typeof value === 'string') {
uint8ArrayValue = decodeAddress(value).publicKey
uint8ArrayValue = encodingUtil.base32ToUint8Array(value).slice(0, ALGORAND_ADDRESS_BYTE_LENGTH - ALGORAND_CHECKSUM_BYTE_LENGTH)
} else if (internal.primitives.isBytes(value)) {
uint8ArrayValue = internal.primitives.getUint8Array(value)
} else {
Expand Down
9 changes: 3 additions & 6 deletions src/impl/global.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Account, Application, Bytes, bytes, internal, op, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import {
DEFAULT_ACCOUNT_MIN_BALANCE,
DEFAULT_ASSET_CREATE_MIN_BALANCE,
Expand All @@ -10,7 +9,7 @@ import {
ZERO_ADDRESS,
} from '../constants'
import { lazyContext } from '../context-helpers/internal-context'
import { asBigInt, getObjectReference } from '../util'
import { getApplicationAddress, getObjectReference } from '../util'

export class GlobalData {
minTxnFee: uint64
Expand Down Expand Up @@ -128,8 +127,7 @@ export const Global: internal.opTypes.GlobalType = {
* Address that the current application controls. Application mode only.
*/
get currentApplicationAddress(): Account {
const appAddress = algosdk.getApplicationAddress(asBigInt(this.currentApplicationId.id))
return Account(Bytes.fromBase32(appAddress))
return this.currentApplicationId.address
},

/**
Expand Down Expand Up @@ -163,8 +161,7 @@ export const Global: internal.opTypes.GlobalType = {
* The application address of the application that called this application. ZeroAddress if this application is at the top-level. Application mode only.
*/
get callerApplicationAddress(): Account {
const appAddress = algosdk.getApplicationAddress(asBigInt(this.callerApplicationId))
return Account(Bytes.fromBase32(appAddress))
return getApplicationAddress(this.callerApplicationId)
},

/**
Expand Down
5 changes: 2 additions & 3 deletions src/subcontexts/contract-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {
internal,
LocalState,
} from '@algorandfoundation/algorand-typescript'
import { ABIMethod } from 'algosdk'
import {
AbiMetadata,
copyAbiMetadatas,
getArc4Signature,
getArc4Selector,
getContractAbiMetadata,
getContractMethodAbiMetadata,
isContractProxy,
Expand Down Expand Up @@ -144,7 +143,7 @@ export class ContractContext {
...args: TParams
): Transaction[] {
const app = lazyContext.ledger.getApplicationForContract(contract)
const methodSelector = abiMetadata ? ABIMethod.fromSignature(getArc4Signature(abiMetadata)).getSelector() : new Uint8Array()
const methodSelector = abiMetadata ? getArc4Selector(abiMetadata) : new Uint8Array()
const { transactions, ...appCallArgs } = extractArraysFromArgs(app, methodSelector, args)
const appTxn = lazyContext.any.txn.applicationCall({
appId: app,
Expand Down
12 changes: 5 additions & 7 deletions src/subcontexts/transaction-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { bytes, Contract, internal, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import { AbiMetadata, getContractMethodAbiMetadata } from '../abi-metadata'
import { TRANSACTION_GROUP_MAX_SIZE } from '../constants'
import { lazyContext } from '../context-helpers/internal-context'
import { DecodedLogs, decodeLogs, LogDecoding } from '../decode-logs'
import { testInvariant } from '../errors'
Expand Down Expand Up @@ -180,10 +180,8 @@ export class TransactionGroup {

constructor(transactions: Transaction[], activeTransactionIndex?: number) {
this.latestTimestamp = Date.now()
if (transactions.length > algosdk.AtomicTransactionComposer.MAX_GROUP_SIZE) {
internal.errors.internalError(
`Transaction group can have at most ${algosdk.AtomicTransactionComposer.MAX_GROUP_SIZE} transactions, as per AVM limits.`,
)
if (transactions.length > TRANSACTION_GROUP_MAX_SIZE) {
internal.errors.internalError(`Transaction group can have at most ${TRANSACTION_GROUP_MAX_SIZE} transactions, as per AVM limits.`)
}
transactions.forEach((txn, index) => Object.assign(txn, { groupIndex: asUint64(index) }))
this.activeTransactionIndex = activeTransactionIndex === undefined ? transactions.length - 1 : activeTransactionIndex
Expand Down Expand Up @@ -245,8 +243,8 @@ export class TransactionGroup {
if (!this.constructingItxnGroup.length) {
internal.errors.internalError('itxn submit without itxn begin')
}
if (this.constructingItxnGroup.length > algosdk.AtomicTransactionComposer.MAX_GROUP_SIZE) {
internal.errors.internalError('Cannot submit more than 16 inner transactions at once')
if (this.constructingItxnGroup.length > TRANSACTION_GROUP_MAX_SIZE) {
internal.errors.internalError(`Cannot submit more than ${TRANSACTION_GROUP_MAX_SIZE} inner transactions at once`)
}
const itxns = this.constructingItxnGroup.map((t) => createInnerTxn(t))
itxns.forEach((itxn, index) => Object.assign(itxn, { groupIndex: asUint64(index) }))
Expand Down
6 changes: 3 additions & 3 deletions src/test-execution-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Account, Application, Asset, Bytes, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import { Account, Application, Asset, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { captureMethodConfig } from './abi-metadata'
import { DecodedLogs, LogDecoding } from './decode-logs'
import * as ops from './impl'
Expand All @@ -19,6 +18,7 @@ import { Box, BoxMap, BoxRef, GlobalState, LocalState } from './impl/state'
import { ContractContext } from './subcontexts/contract-context'
import { LedgerContext } from './subcontexts/ledger-context'
import { TransactionContext } from './subcontexts/transaction-context'
import { getRandomBytes } from './util'
import { ValueGenerator } from './value-generators'

export class TestExecutionContext implements internal.ExecutionContext {
Expand All @@ -34,7 +34,7 @@ export class TestExecutionContext implements internal.ExecutionContext {
this.#ledgerContext = new LedgerContext()
this.#txnContext = new TransactionContext()
this.#valueGenerator = new ValueGenerator()
this.#defaultSender = Account(defaultSenderAddress ?? Bytes.fromBase32(algosdk.generateAccount().addr))
this.#defaultSender = Account(defaultSenderAddress ?? getRandomBytes(32).asAlgoTs())
}

account(address?: bytes): Account {
Expand Down
39 changes: 37 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { Bytes, bytes, internal } from '@algorandfoundation/algorand-typescript'
import { Account, Bytes, bytes, internal } from '@algorandfoundation/algorand-typescript'
import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
import { encodingUtil } from '@algorandfoundation/puya-ts'
import { randomBytes } from 'crypto'
import { BITS_IN_BYTE, MAX_BYTES_SIZE, MAX_UINT512, MAX_UINT8, UINT512_SIZE } from './constants'
import { sha512_256 as js_sha512_256 } from 'js-sha512'
import {
ALGORAND_ADDRESS_BYTE_LENGTH,
ALGORAND_ADDRESS_LENGTH,
ALGORAND_CHECKSUM_BYTE_LENGTH,
APP_ID_PREFIX,
BITS_IN_BYTE,
HASH_BYTES_LENGTH,
MAX_BYTES_SIZE,
MAX_UINT512,
MAX_UINT8,
UINT512_SIZE,
} from './constants'
import { BytesBackedCls, Uint64BackedCls } from './impl/base'
import { DeliberateAny } from './typescript-helpers'

Expand Down Expand Up @@ -192,3 +205,25 @@ export const conactUint8Arrays = (...values: Uint8Array[]): Uint8Array => {
export const uint8ArrayToNumber = (value: Uint8Array): number => {
return value.reduce((acc, x) => acc * 256 + x, 0)
}

export const checksumFromPublicKey = (pk: Uint8Array): Uint8Array => {
return Uint8Array.from(js_sha512_256.array(pk).slice(HASH_BYTES_LENGTH - ALGORAND_CHECKSUM_BYTE_LENGTH, HASH_BYTES_LENGTH))
}

export const getApplicationAddress = (appId: internal.primitives.StubUint64Compat): Account => {
const toBeSigned = conactUint8Arrays(asUint8Array(APP_ID_PREFIX), encodingUtil.bigIntToUint8Array(asBigInt(appId), 8))
const appIdHash = js_sha512_256.array(toBeSigned)
const publicKey = Uint8Array.from(appIdHash)
const address = encodeAddress(publicKey)
return Account(Bytes.fromBase32(address))
}

export const encodeAddress = (address: Uint8Array): string => {
const checksum = checksumFromPublicKey(address)
return encodingUtil.uint8ArrayToBase32(conactUint8Arrays(address, checksum)).slice(0, ALGORAND_ADDRESS_LENGTH)
}

export const decodePublicKey = (address: string): Uint8Array => {
const decoded = encodingUtil.base32ToUint8Array(address)
return decoded.slice(0, ALGORAND_ADDRESS_BYTE_LENGTH - ALGORAND_CHECKSUM_BYTE_LENGTH)
}
5 changes: 2 additions & 3 deletions src/value-generators/avm.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Account, Application, Asset, bytes, Bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'
import { randomBytes } from 'crypto'
import { MAX_BYTES_SIZE, MAX_UINT64, ZERO_ADDRESS } from '../constants'
import { lazyContext } from '../context-helpers/internal-context'
import { AccountData } from '../impl/account'
import { ApplicationCls, ApplicationData } from '../impl/application'
import { AssetCls, AssetData } from '../impl/asset'
import { asBigInt, asUint64Cls, getRandomBigInt } from '../util'
import { asBigInt, asUint64Cls, getRandomBigInt, getRandomBytes } from '../util'

type AccountContextData = Partial<AccountData['account']> & {
address?: Account
Expand Down Expand Up @@ -37,7 +36,7 @@ export class AvmValueGenerator {
}

account(input?: AccountContextData): Account {
const account = input?.address ?? Account(Bytes.fromBase32(algosdk.generateAccount().addr))
const account = input?.address ?? Account(getRandomBytes(32).asAlgoTs())

if (input?.address && lazyContext.ledger.accountDataMap.has(account)) {
internal.errors.internalError(
Expand Down
15 changes: 8 additions & 7 deletions tests/arc4/address.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getABIEncodedValue } from '@algorandfoundation/algokit-utils/types/app-arc56'
import { Account, Bytes } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { Address, interpretAsArc4 } from '@algorandfoundation/algorand-typescript/arc4'
import { ABIType, encodeAddress } from 'algosdk'
import { afterEach, describe, expect, test } from 'vitest'
import { ABI_RETURN_VALUE_LOG_PREFIX } from '../../src/constants'
import { encodeAddress } from '../../src/util'
import { asUint8Array } from '../util'

const abiAddressType = ABIType.from('address')
const abiTypeString = 'address'
const testData = [
Bytes.fromHex('00'.repeat(32)),
Bytes.fromHex('01'.repeat(32)),
Expand All @@ -21,19 +22,19 @@ describe('arc4.Address', async () => {
})

test.each(testData)('create address from bytes', async (value) => {
const sdkResult = abiAddressType.encode(asUint8Array(value))
const sdkResult = getABIEncodedValue(asUint8Array(value), abiTypeString, {})
const result = new Address(value)
expect(result.bytes).toEqual(sdkResult)
})
test.each(testData)('create address from str', async (value) => {
const stringValue = encodeAddress(asUint8Array(value))
const sdkResult = abiAddressType.encode(stringValue)
const sdkResult = getABIEncodedValue(stringValue, abiTypeString, {})
const result = new Address(stringValue)
expect(result.bytes).toEqual(sdkResult)
})
test.each(testData)('create address from Account', async (value) => {
const accountValue = Account(value)
const sdkResult = abiAddressType.encode(asUint8Array(accountValue.bytes))
const sdkResult = getABIEncodedValue(asUint8Array(accountValue.bytes), abiTypeString, {})
const result = new Address(accountValue)
expect(result.bytes).toEqual(sdkResult)
})
Expand Down Expand Up @@ -69,13 +70,13 @@ describe('arc4.Address', async () => {
})

test.each(testData)('fromBytes method', async (value) => {
const sdkResult = abiAddressType.encode(asUint8Array(value))
const sdkResult = getABIEncodedValue(asUint8Array(value), abiTypeString, {})
const result = interpretAsArc4<Address>(value)
expect(result.bytes).toEqual(sdkResult)
})

test.each(testData)('fromLog method', async (value) => {
const sdkResult = abiAddressType.encode(asUint8Array(value))
const sdkResult = getABIEncodedValue(asUint8Array(value), abiTypeString, {})
const paddedValue = Bytes([...asUint8Array(ABI_RETURN_VALUE_LOG_PREFIX), ...asUint8Array(value)])
const result = interpretAsArc4<Address>(paddedValue, 'log')
expect(result.bytes).toEqual(sdkResult)
Expand Down
Loading

0 comments on commit ed4fd56

Please sign in to comment.