From 55a1dd615dc58b8af80d84a74ee5f2c1ff0da374 Mon Sep 17 00:00:00 2001 From: martin machiebe Date: Wed, 27 Nov 2024 22:24:43 +0100 Subject: [PATCH] add lending protocol withdraw and deposit --- lending-protocol/.gitignore | 2 + lending-protocol/README.md | 25 +++ lending-protocol/alephium.config.ts | 50 +++++ lending-protocol/contracts/authorizations.ral | 19 ++ .../contracts/lending-account.ral | 53 ++++++ .../lending-protocol-account-factory.ral | 37 ++++ .../contracts/lending-protocol.ral | 80 ++++++++ lending-protocol/jest-config.json | 7 + lending-protocol/package.json | 48 +++++ lending-protocol/scripts/0_deploy_faucet.ts | 30 +++ lending-protocol/src/token.ts | 48 +++++ .../test/lending-protocol.test.ts | 102 ++++++++++ lending-protocol/test/utils.ts | 174 ++++++++++++++++++ .../tsconfig.json | 0 14 files changed, 675 insertions(+) create mode 100644 lending-protocol/.gitignore create mode 100644 lending-protocol/README.md create mode 100644 lending-protocol/alephium.config.ts create mode 100644 lending-protocol/contracts/authorizations.ral create mode 100644 lending-protocol/contracts/lending-account.ral create mode 100644 lending-protocol/contracts/lending-protocol-account-factory.ral create mode 100644 lending-protocol/contracts/lending-protocol.ral create mode 100644 lending-protocol/jest-config.json create mode 100644 lending-protocol/package.json create mode 100644 lending-protocol/scripts/0_deploy_faucet.ts create mode 100644 lending-protocol/src/token.ts create mode 100644 lending-protocol/test/lending-protocol.test.ts create mode 100644 lending-protocol/test/utils.ts rename {dynamic-array => lending-protocol}/tsconfig.json (100%) diff --git a/lending-protocol/.gitignore b/lending-protocol/.gitignore new file mode 100644 index 0000000..572406b --- /dev/null +++ b/lending-protocol/.gitignore @@ -0,0 +1,2 @@ +/node_modules +package-lock.json \ No newline at end of file diff --git a/lending-protocol/README.md b/lending-protocol/README.md new file mode 100644 index 0000000..514fd4b --- /dev/null +++ b/lending-protocol/README.md @@ -0,0 +1,25 @@ +# My dApp Template + +## Install + +``` +npm install +``` + +## Start a local devnet for testing and development + +Please refer to the documentation here: https://wiki.alephium.org/full-node/devnet + +## Compile + +Compile the TypeScript files into JavaScript: + +``` +npm run compile +``` + +## Testing + +``` +npm run test +``` diff --git a/lending-protocol/alephium.config.ts b/lending-protocol/alephium.config.ts new file mode 100644 index 0000000..9660925 --- /dev/null +++ b/lending-protocol/alephium.config.ts @@ -0,0 +1,50 @@ +import { Configuration } from '@alephium/cli' +import { Number256 } from '@alephium/web3' + +// Settings are usually for configuring +export type Settings = { + issueTokenAmount: Number256 + openaiAPIKey?: string + ipfs?: { + infura?: { + projectId: string, + projectSecret: string + } + } +} + +const defaultSettings: Settings = { + issueTokenAmount: 100n, + openaiAPIKey: process.env.OPENAI_API_KEY || '', + ipfs: { + infura: { + projectId: process.env.IPFS_INFURA_PROJECT_ID || '', + projectSecret: process.env.IPFS_INFURA_PROJECT_SECRET || '' + } + } +} + +const configuration: Configuration = { + networks: { + devnet: { + nodeUrl: 'http://127.0.0.1:22973', + // here we could configure which address groups to deploy the contract + privateKeys: ['a642942e67258589cd2b1822c631506632db5a12aabcf413604e785300d762a5'], + settings: defaultSettings + }, + + testnet: { + nodeUrl: process.env.NODE_URL as string, + privateKeys: process.env.PRIVATE_KEYS === undefined ? [] : process.env.PRIVATE_KEYS.split(','), + settings: defaultSettings + }, + + mainnet: { + nodeUrl: process.env.NODE_URL as string, + privateKeys: process.env.PRIVATE_KEYS === undefined ? [] : process.env.PRIVATE_KEYS.split(','), + settings: defaultSettings + } + } +} + +export default configuration \ No newline at end of file diff --git a/lending-protocol/contracts/authorizations.ral b/lending-protocol/contracts/authorizations.ral new file mode 100644 index 0000000..f10c0cf --- /dev/null +++ b/lending-protocol/contracts/authorizations.ral @@ -0,0 +1,19 @@ +Abstract Contract Authorization( + mut owner_: Address +) { + enum AuthorizationErrorCodes { + UNAUTHORIZED = 401 + } + + fn onlyOwner(caller: Address) -> () { + checkCaller!(callerAddress!() == selfAddress!(), AuthorizationErrorCodes.UNAUTHORIZED) + assert!(caller == owner_, AuthorizationErrorCodes.UNAUTHORIZED) + } + + @using(updateFields = true) + pub fn changeOwner(newOwner: Address) -> () { + onlyOwner(callerAddress!()) + + owner_ = newOwner + } +} \ No newline at end of file diff --git a/lending-protocol/contracts/lending-account.ral b/lending-protocol/contracts/lending-account.ral new file mode 100644 index 0000000..a5af713 --- /dev/null +++ b/lending-protocol/contracts/lending-account.ral @@ -0,0 +1,53 @@ +Contract LendingProtocolAccount( + tokenId: ByteVec, + _address: Address, + parentContractAddress: Address, + mut amountDeposited: U256 +) { + enum ErrorCodes { + UNAUTHORIZED = 402 + INSUFFICIENT_BALANCE = 423 + INVALID_AMOUNT = 424 + } + + pub fn getTokenId() -> ByteVec { + return tokenId + } + + pub fn getUser() -> Address { + return _address + } + + pub fn getTotalDeposit() -> U256 { + return amountDeposited + } + + @using(assetsInContract = true, preapprovedAssets = true, updateFields = true) + pub fn deposit(amount: U256) -> () { + let caller = callerAddress!() + + checkCaller!(caller == parentContractAddress, ErrorCodes.UNAUTHORIZED) + assert!(amount > 0, ErrorCodes.INVALID_AMOUNT) + transferTokenToSelf!(_address, tokenId, amount) + + amountDeposited = amountDeposited + amount + } + + @using(assetsInContract = true, updateFields = true) + pub fn withdraw(amount: U256) -> () { + let caller = callerAddress!() + + checkCaller!(caller == parentContractAddress, ErrorCodes.UNAUTHORIZED) + assert!(amount > 0, ErrorCodes.INVALID_AMOUNT) + assert!(amountDeposited >= amount, ErrorCodes.INSUFFICIENT_BALANCE) + + amountDeposited = amountDeposited - amount + + transferTokenFromSelf!(_address, tokenId, amount) + + if (amountDeposited == 0) { + destroySelf!(_address) + } + } + +} \ No newline at end of file diff --git a/lending-protocol/contracts/lending-protocol-account-factory.ral b/lending-protocol/contracts/lending-protocol-account-factory.ral new file mode 100644 index 0000000..fb7deda --- /dev/null +++ b/lending-protocol/contracts/lending-protocol-account-factory.ral @@ -0,0 +1,37 @@ +Abstract Contract LendingProtocolAccountFactory( + tokenId: ByteVec, + lendingProtocolAccountTemplateId: ByteVec +) { + enum ErrorCodes { + DUPLICATE_ERROR = 422 + } + + pub fn lendingProtocolAccountExists(_address: Address) -> Bool { + let lendingProtocolAccountId = subContractId!(toByteVec!(_address)) + return contractExists!(lendingProtocolAccountId) + } + + pub fn getLendingProtocolAccount(_address: Address) -> LendingProtocolAccount { + let lendingProtocolAccountId = subContractId!(toByteVec!(_address)) + return LendingProtocolAccount(lendingProtocolAccountId) + } + + @using(preapprovedAssets = true) + fn createLendingProtocolAccount(_address: Address, amount: U256) -> () { + assert!(!lendingProtocolAccountExists(_address), ErrorCodes.DUPLICATE_ERROR) + + let (encodedImmFields, encodedMutFields) = LendingProtocolAccount.encodeFields!( + tokenId, + _address, + selfAddress!(), + amount + ) + + let _ = copyCreateSubContract!{_address -> ALPH: 1 alph, tokenId: amount}( + toByteVec!(_address), + lendingProtocolAccountTemplateId, + encodedImmFields, + encodedMutFields + ) + } +} \ No newline at end of file diff --git a/lending-protocol/contracts/lending-protocol.ral b/lending-protocol/contracts/lending-protocol.ral new file mode 100644 index 0000000..48f4dda --- /dev/null +++ b/lending-protocol/contracts/lending-protocol.ral @@ -0,0 +1,80 @@ +Contract LendingProtocol( + tokenId: ByteVec, + lendingProtocolAccountTemplateId: ByteVec, + mut amountDeposited: U256 + mut owner_: Address +) extends LendingProtocolAccountFactory(tokenId, lendingProtocolTemplateId) { + //////////////////////// + // Events + //////////////////////// + + event Deposit(_address: Address, amount: U256) + event Withdrawal(_address: Address, amount: U256) + + //////////////////////// + // Error Codes + //////////////////////// + + enum ErrorCodes { + INVALID_AMOUNT = 0 + } + + //////////////////////// + // Public Functions + //////////////////////// + + pub fn getTokenId() -> ByteVec { + return tokenId + } + + @using(preapprovedAssets = true, updateFields = true, checkExternalCaller = false) + pub fn deposit(amount: U256) -> () { + let _address = callerAddress!() + + assert!(amount > 0, ErrorCodes.INVALID_AMOUNT) + if (lendingProtocolAccountExists(_address)) { + let lendingProtocolAccount = getLendingProtocolAccount(_address) + lendingProtocolAccount.deposit{_address -> tokenId: amount}(amount) + } else { + createLendingProtocolAccount{_address -> ALPH: 1 alph, tokenId: amount}(_address, amount) + } + + amountDeposited = amountDeposited + amount + emit Deposit(_address, amount) + } + + @using(updateFields = true, checkExternalCaller = false) + pub fn withdraw(amount: U256) -> () { + let _address = callerAddress!() + + let lendingProtocolAccount = getLendingProtocolAccount(_address) + + lendingProtocolAccount.withdraw(amount) + + amountDeposited = amountDeposited - amount + emit Withdrawal(_address, amount) + } + + + pub fn upgrade(newBytecode: ByteVec) -> () { + onlyOwner(callerAddress!()) + + migrate!(newBytecode) + } +} + +TxScript Deposit(lendingProtocol: LendingProtocol, amount: U256) { + let _address = callerAddress!() + let tokenId = lendingProtocol.getTokenId() + let lendingProtocolAccExists = lendingProtocol.lendingProtocolAccountExists(_address) + + if (lendingProtocolAccExists) { + lendingProtocol.deposit{_address -> tokenId: amount}(amount) + } else { + lendingProtocol.deposit{_address -> tokenId: amount, ALPH: 1 alph}(amount) + } +} + +TxScript Withdrawal(lendingProtocol: LendingProtocol, amount: U256) { + lendingProtocol.withdraw(amount) +} diff --git a/lending-protocol/jest-config.json b/lending-protocol/jest-config.json new file mode 100644 index 0000000..eef4a26 --- /dev/null +++ b/lending-protocol/jest-config.json @@ -0,0 +1,7 @@ +{ + "testPathIgnorePatterns": [".*/node_modules/", ".*/dist/.*"], + "transform": { + "^.+\\.(t|j)sx?$": "ts-jest" + }, + "testMatch": ["/**/*.test.ts"] +} diff --git a/lending-protocol/package.json b/lending-protocol/package.json new file mode 100644 index 0000000..786ccee --- /dev/null +++ b/lending-protocol/package.json @@ -0,0 +1,48 @@ +{ + "name": "my-dapp-template", + "version": "0.1.0", + "license": "GPL", + "scripts": { + "build": "npm run clean && npx --yes tsc --build .", + "clean": "npm run clean:windows && npm run clean:unix", + "clean:unix": "node -e \"if (process.platform !== 'win32') process.exit(1)\" || rm -rf dist", + "clean:windows": "node -e \"if (process.platform === 'win32') process.exit(1)\" || , if exist dist rmdir /Q /S dist", + "compile": "npx cli compile", + "deploy": "npx cli deploy", + "lint": "eslint . --ext ts", + "lint:fix": "eslint . --fix --ext ts", + "test": "jest -i --config ./jest-config.json" + }, + "dependencies": { + "@alephium/cli": "^1.8.0", + "@alephium/web3": "^1.8.0", + "@alephium/web3-test": "^1.8.0", + "@alephium/web3-wallet": "^1.8.0" + }, + "devDependencies": { + "@types/jest": "^27.5.1", + "@types/node": "^16.18.23", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "eslint": "^8.37.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.0", + "ts-jest": "^28.0.2", + "ts-node": "^10.7.0", + "typescript": "^4.4.2" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + }, + "prettier": { + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "bracketSameLine": false, + "trailingComma": "none" + } +} diff --git a/lending-protocol/scripts/0_deploy_faucet.ts b/lending-protocol/scripts/0_deploy_faucet.ts new file mode 100644 index 0000000..389ae2d --- /dev/null +++ b/lending-protocol/scripts/0_deploy_faucet.ts @@ -0,0 +1,30 @@ +import { Deployer, DeployFunction, Network } from '@alephium/cli' +import { Settings } from '../alephium.config' +import { TokenFaucet } from '../artifacts/ts' +import { stringToHex } from '@alephium/web3' + +// This deploy function will be called by cli deployment tool automatically +// Note that deployment scripts should prefixed with numbers (starting from 0) +const deployFaucet: DeployFunction = async ( + deployer: Deployer, + network: Network +): Promise => { + // Get settings + const issueTokenAmount = network.settings.issueTokenAmount + const result = await deployer.deployContract(TokenFaucet, { + // The amount of token to be issued + issueTokenAmount: issueTokenAmount, + // The initial states of the faucet contract + initialFields: { + symbol: stringToHex('TF'), + name: stringToHex('TokenFaucet'), + decimals: 18n, + supply: issueTokenAmount, + balance: issueTokenAmount + } + }) + console.log('Token faucet contract id: ' + result.contractInstance.contractId) + console.log('Token faucet contract address: ' + result.contractInstance.address) +} + +export default deployFaucet diff --git a/lending-protocol/src/token.ts b/lending-protocol/src/token.ts new file mode 100644 index 0000000..8964395 --- /dev/null +++ b/lending-protocol/src/token.ts @@ -0,0 +1,48 @@ +import { Deployments } from '@alephium/cli' +import { web3, DUST_AMOUNT } from '@alephium/web3' +import { testNodeWallet } from '@alephium/web3-test' +import configuration from '../alephium.config' +import { TokenFaucet } from '../artifacts/ts' + +async function withdraw() { + web3.setCurrentNodeProvider('http://127.0.0.1:22973') + + // Attention: test wallet is used for demonstration purpose + const signer = await testNodeWallet() + + const deployments = await Deployments.load(configuration, 'devnet') + + // The test wallet has four accounts with one in each address group + // The wallet calls withdraw function for all of the address groups + for (const account of await signer.getAccounts()) { + // Set an active account to prepare and sign transactions + await signer.setSelectedAccount(account.address) + const accountGroup = account.group + + // Load the metadata of the deployed contract in the right group + const faucet = deployments.getInstance(TokenFaucet, accountGroup) + if (faucet === undefined) { + console.log(`The contract is not deployed on group ${account.group}`) + continue + } + const tokenId = faucet.contractId + const tokenAddress = faucet.address + console.log(`Token faucet contract id: ${tokenId}`) + console.log(`Token faucet contract address: ${tokenAddress}`) + + // Submit a transaction to use the transaction script + await faucet.transact.withdraw({ + signer: signer, + attoAlphAmount: DUST_AMOUNT * 2n, + args: { + amount: 1n + }, + }) + + // Fetch the latest state of the token contract + const state = await faucet.fetchState() + console.log(JSON.stringify(state.fields, null, ' ')) + } +} + +withdraw() diff --git a/lending-protocol/test/lending-protocol.test.ts b/lending-protocol/test/lending-protocol.test.ts new file mode 100644 index 0000000..0f38937 --- /dev/null +++ b/lending-protocol/test/lending-protocol.test.ts @@ -0,0 +1,102 @@ +import { + ALPH_TOKEN_ID, + Address, + ONE_ALPH, + addressFromContractId, + groupOfAddress, + sleep, + subContractId +} from '@alephium/web3' +import { LendingProtocol, LendingProtocolInstance, LendingProtocolAccount } from '../artifacts/ts' +import { + deployLendingProtocol, + deposit, + depositFailed, + withdrawal, + balanceOf, + randomP2PKHAddress, + transferTokenTo, + checkLendingProtocolAccount, + transferAlphTo, + alph +} from './utils' +import { PrivateKeyWallet } from '@alephium/web3-wallet' +import { testAddress } from '@alephium/web3-test' +import * as base58 from 'bs58' + +describe('test lendingProtocol', () => { + const groupIndex = groupOfAddress(testAddress) + + let lendingProtocol: LendingProtocolInstance + let owner: Address + let tokenId: string + let _addresses: PrivateKeyWallet[] + let ownerWallet: PrivateKeyWallet + + beforeEach(async () => { + ownerWallet = PrivateKeyWallet.Random(groupIndex) + owner = ownerWallet.address + lendingProtocol = (await deployLendingProtocol(owner)).contractInstance + + const lendingProtocolState = await lendingProtocol.fetchState() + tokenId = lendingProtocolState.fields.tokenId + + _addresses = Array.from(Array(2).keys()).map((_) => PrivateKeyWallet.Random(groupIndex)) + // Setup initial token balances for _addresses + for (const _address of _addresses) { + await transferAlphTo(_address.address, alph(1000)) + await transferTokenTo(_address.address, tokenId, 1000n) + } + + await transferAlphTo(owner, alph(1000)) + await transferTokenTo(owner, tokenId, 1000n) + }) + + async function checkAmountDeposited(expectedAmount: bigint) { + const state = await lendingProtocol.fetchState() + expect(state.fields.amountDeposited).toEqual(expectedAmount) + } + + test('deposit:failed scenarios', async () => { + const [_address] = _addresses + + // Test lendingProtocol with 0 amount + await depositFailed( + _address, + lendingProtocol, + 0n, + tokenId, + Number(LendingProtocol.consts.ErrorCodes.INVALID_AMOUNT) + ) + }) + + test('deposit:successful lendingProtocol', async () => { + const [_address1, _address2] = _addresses + const depositAmount = 100n + // Test initial deposit + await deposit(_address1, lendingProtocol, depositAmount, tokenId) + // await checklendingProtocolAccount(lendingProtocol, _address1.address, depositAmount) + await checkAmountDeposited(depositAmount) + + // Test additional deposit + await deposit(_address1, lendingProtocol, depositAmount, tokenId) + // await checklendingProtocolAccount(lendingProtocol, _address1.address, depositAmount * 2n) + await checkAmountDeposited(depositAmount * 2n) + + // Test multiple _addresses + await deposit(_address2, lendingProtocol, depositAmount, tokenId) + // await checklendingProtocolAccount(lendingProtocol, _address2.address, depositAmount) + await checkAmountDeposited(depositAmount * 3n) + }) + + test('withdrawal', async () => { + const [_address] = _addresses + const depositAmount = 100n + await deposit(_address, lendingProtocol, depositAmount, tokenId) + + await withdrawal(_address, lendingProtocol, depositAmount) + + const finalBalance = await balanceOf(tokenId, _address.address) + expect(finalBalance).toEqual(1000n) + }) +}) diff --git a/lending-protocol/test/utils.ts b/lending-protocol/test/utils.ts new file mode 100644 index 0000000..66f96f9 --- /dev/null +++ b/lending-protocol/test/utils.ts @@ -0,0 +1,174 @@ +import { expectAssertionError, testAddress, testPrivateKey } from '@alephium/web3-test' +import { + LendingProtocol, + LendingProtocolInstance, + LendingProtocolAccount, + Deposit, + Withdrawal, +} from '../artifacts/ts' +import { PrivateKeyWallet } from '@alephium/web3-wallet' +import { + ALPH_TOKEN_ID, + Address, + DUST_AMOUNT, + ONE_ALPH, + SignerProvider, + groupOfAddress, + web3, + waitForTxConfirmation, + addressFromContractId, + subContractId +} from '@alephium/web3' +import { randomBytes } from 'crypto' +import * as base58 from 'bs58' + +web3.setCurrentNodeProvider('http://127.0.0.1:22973', undefined, fetch) +export const ZERO_ADDRESS = 'tgx7VNFoP9DJiFMFgXXtafQZkUvyEdDHT9ryamHJYrjq' +export const defaultSigner = new PrivateKeyWallet({ privateKey: testPrivateKey }) + +async function waitTxConfirmed(promise: Promise): Promise { + const result = await promise + await waitForTxConfirmation(result.txId, 1, 1000) + return result +} + +export function randomP2PKHAddress(groupIndex = 0): string { + const prefix = Buffer.from([0x00]) + const bytes = Buffer.concat([prefix, randomBytes(32)]) + const address = base58.encode(bytes) + if (groupOfAddress(address) === groupIndex) { + return address + } + return randomP2PKHAddress(groupIndex) +} + +export async function deployLendingProtocolAccountTemplate(tokenId: string) { + return await LendingProtocolAccount.deploy(defaultSigner, { + initialFields: { + tokenId, + _address: ZERO_ADDRESS, + parentContractAddress: ZERO_ADDRESS, + amountDeposited: 0n, + rewards: 0n, + } + }) +} + +export async function deployTestTokens(amount: bigint) { + const lendingProtocolToken = await deployTestToken(amount) + const rewardsToken = await deployTestToken(amount) + return { lendingProtocolToken, rewardsToken } +} + +export async function deployLendingProtocol( + owner: Address +) { + const { lendingProtocolToken } = await deployTestTokens(1000000n) + + const accountTemplate = await deployLendingProtocolAccountTemplate( + lendingProtocolToken.contractInstance.contractId + ) + + return await LendingProtocol.deploy(defaultSigner, { + initialFields: { + tokenId: lendingProtocolToken.contractInstance.contractId, + lendingProtocolAccountTemplateId: accountTemplate.contractInstance.contractId, + amountDeposited: 0n, + owner_: owner + }, + initialTokenAmounts: [ + { id: lendingProtocolToken.contractInstance.contractId, amount: 1000000n } + ] + }) +} + +export async function transferTokenTo(to: Address, tokenId: string, amount: bigint) { + return await waitTxConfirmed( + defaultSigner.signAndSubmitTransferTx({ + signerAddress: testAddress, + destinations: [ + { + address: to, + attoAlphAmount: ONE_ALPH, + tokens: [{ id: tokenId, amount: amount }] + } + ] + }) + ) +} + +export async function transferAlphTo(to: Address, amount: bigint) { + return await waitTxConfirmed( + defaultSigner.signAndSubmitTransferTx({ + signerAddress: testAddress, + destinations: [{ address: to, attoAlphAmount: amount }] + }) + ) +} + +export function alph(amount: bigint | number): bigint { + return BigInt(amount) * ONE_ALPH +} + +export async function deposit( + signer: SignerProvider, + lendingProtocol: LendingProtocolInstance, + amount: bigint, + tokenId: string +) { + return await Deposit.execute(signer, { + initialFields: { + lendingProtocol: lendingProtocol.contractId, + amount + }, + tokens: [{ id: tokenId, amount }], + attoAlphAmount: ONE_ALPH + }) +} + +export async function depositFailed( + signer: SignerProvider, + lendingProtocol: LendingProtocolInstance, + amount: bigint, + tokenId: string, + errorCode: number +) { + await expectAssertionError( + deposit(signer, lendingProtocol, amount, tokenId), + lendingProtocol.address, + errorCode + ) +} + +export async function withdrawal( + signer: SignerProvider, + lendingProtocol: LendingProtocolInstance, + amount: bigint, +) { + return await Withdrawal.execute(signer, { + initialFields: { + lendingProtocol: lendingProtocol.contractId, + amount + } + }) +} + +export async function checkLendingProtocolAccount( + lendingProtocol: LendingProtocolInstance, + _address: Address, + expectedAmount: bigint +) { + const groupIndex = groupOfAddress(_address) + const path = base58.decode(_address) + const accountId = subContractId(lendingProtocol.contractId, path, groupIndex) + const lendingProtocolAccount = LendingProtocolAccount.at(addressFromContractId(accountId)) + const state = await lendingProtocolAccount.fetchState() + expect(state.fields.amountDeposited).toEqual(expectedAmount) +} + +export async function balanceOf(tokenId: string, address = testAddress): Promise { + const balances = await web3.getCurrentNodeProvider().addresses.getAddressesAddressBalance(address) + if (tokenId === ALPH_TOKEN_ID) return BigInt(balances.balance) + const balance = balances.tokenBalances?.find((t) => t.id === tokenId) + return balance === undefined ? 0n : BigInt(balance.amount) +} diff --git a/dynamic-array/tsconfig.json b/lending-protocol/tsconfig.json similarity index 100% rename from dynamic-array/tsconfig.json rename to lending-protocol/tsconfig.json