diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index f1df872..081581b 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -58,7 +58,7 @@ jobs: run: npm run lint:scripts unit-tests: - name: Unit tests + name: Integration tests (TestNet) runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/package.json b/package.json index ce1440a..65c5495 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,10 @@ { "type": "chore", "release": "patch" + }, + { + "type": "refactor", + "release": "patch" } ] } diff --git a/src/clients/SubtopiaClient.ts b/src/clients/SubtopiaClient.ts index 570cd9a..54e2011 100644 --- a/src/clients/SubtopiaClient.ts +++ b/src/clients/SubtopiaClient.ts @@ -42,6 +42,7 @@ import { DiscountType, Duration, SubscriptionType, + LifecycleState, } from "../enums"; import { @@ -198,6 +199,52 @@ export class SubtopiaClient { }); } + protected async updateLifecycle({ + lifecycle, + }: { + lifecycle: LifecycleState; + }): Promise<{ + txID: string; + }> { + const updateLifecycleAtc = new AtomicTransactionComposer(); + updateLifecycleAtc.addMethodCall({ + appID: this.appID, + method: new ABIMethod({ + name: "update_lifecycle", + args: [ + { + type: "uint64", + name: "lifecycle", + desc: "The new lifecycle.", + }, + ], + returns: { type: "void" }, + }), + methodArgs: [lifecycle], + sender: this.creator.addr, + signer: this.creator.signer, + suggestedParams: await getParamsWithFeeCount(this.algodClient, 1), + }); + + const response = await updateLifecycleAtc.execute(this.algodClient, 10); + + return { + txID: response.txIDs.pop() as string, + }; + } + + public async enable(): Promise<{ + txID: string; + }> { + return this.updateLifecycle({ lifecycle: LifecycleState.ENABLED }); + } + + public async disable(): Promise<{ + txID: string; + }> { + return this.updateLifecycle({ lifecycle: LifecycleState.DISABLED }); + } + public async getAppState( withPriceNormalization = true ): Promise { diff --git a/src/clients/SubtopiaRegistryClient.ts b/src/clients/SubtopiaRegistryClient.ts index ab8898a..88692b9 100644 --- a/src/clients/SubtopiaRegistryClient.ts +++ b/src/clients/SubtopiaRegistryClient.ts @@ -386,7 +386,7 @@ export class SubtopiaRegistryClient { { type: "application", name: "infrastructure", - desc: "The INFRASTRUCTURE.", + desc: "The product.", }, { type: "application", @@ -498,12 +498,12 @@ export class SubtopiaRegistryClient { { type: "uint64", name: "sub_type", - desc: "The sub type of the INFRASTRUCTURE.", + desc: "The sub type of The product.", }, { type: "uint64", name: "price", - desc: "The price of the INFRASTRUCTURE.", + desc: "The price of The product.", }, { type: "uint64", @@ -513,17 +513,17 @@ export class SubtopiaRegistryClient { { type: "asset", name: "coin", - desc: "The coin of the INFRASTRUCTURE.", + desc: "The coin of The product.", }, { type: "string", name: "unit_name", - desc: "The unit name of the INFRASTRUCTURE.", + desc: "The unit name of The product.", }, { type: "string", name: "image_url", - desc: "The image URL of the INFRASTRUCTURE.", + desc: "The image URL of The product.", }, { type: "address", @@ -625,4 +625,54 @@ export class SubtopiaRegistryClient { infrastructureID: Number(response.methodResults[0].returnValue), }; } + + public async deleteInfrastructure({ + infrastructureID, + lockerID, + }: { + infrastructureID: number; + lockerID: number; + }): Promise<{ + txID: string; + }> { + const deleteInfraAtc = new AtomicTransactionComposer(); + deleteInfraAtc.addMethodCall({ + appID: this.appID, + method: new ABIMethod({ + name: "delete_infrastructure", + args: [ + { + type: "application", + name: "infrastructure", + desc: "The product.", + }, + { + type: "application", + name: "locker", + desc: "The locker.", + }, + ], + returns: { type: "void" }, + }), + methodArgs: [infrastructureID, lockerID], + boxes: [ + { + appIndex: this.appID, + name: new Uint8Array([ + ...Buffer.from("cl-"), + ...decodeAddress(this.creator.addr).publicKey, + ]), + }, + ], + sender: this.creator.addr, + signer: this.creator.signer, + suggestedParams: await getParamsWithFeeCount(this.algodClient, 4), + }); + + const response = await deleteInfraAtc.execute(this.algodClient, 10); + + return { + txID: response.txIDs.pop() as string, + }; + } } diff --git a/src/enums/index.ts b/src/enums/index.ts index 38906f5..41b2aec 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -43,3 +43,8 @@ export enum LockerType { CREATOR = 0, USER = 1, } + +export enum LifecycleState { + ENABLED = 0, + DISABLED = 1, +} diff --git a/tests/subtopia.test.ts b/tests/subtopia.test.ts index 90183d5..5a8cf66 100644 --- a/tests/subtopia.test.ts +++ b/tests/subtopia.test.ts @@ -26,27 +26,30 @@ import { transactionSignerAccount, transferAlgos, } from "@algorandfoundation/algokit-utils"; +import { TransactionSignerAccount } from "@algorandfoundation/algokit-utils/types/account"; + +const CONFIG = { + SERVER_URL: "https://testnet-api.algonode.cloud", + DISPENSER_MNEMONIC: process.env[ + "TESTNET_SUBTOPIA_DISPENSER_MNEMONIC" + ] as string, + CREATOR_MNEMONIC: process.env["TESTNET_SUBTOPIA_CREATOR_MNEMONIC"] as string, + BOB_MNEMONIC: process.env["TESTNET_SUBTOPIA_BOB_MNEMONIC"] as string, +}; const algodClient = getAlgoClient({ - server: "https://testnet-api.algonode.cloud", + server: CONFIG.SERVER_URL, }); -const dispenserAccount = mnemonicToSecretKey( - process.env["TESTNET_SUBTOPIA_DISPENSER_MNEMONIC"] as string -); +const dispenserAccount = mnemonicToSecretKey(CONFIG.DISPENSER_MNEMONIC); +const creatorAccount = mnemonicToSecretKey(CONFIG.CREATOR_MNEMONIC); +const bobTestAccount = mnemonicToSecretKey(CONFIG.BOB_MNEMONIC); -const creatorAccount = mnemonicToSecretKey( - process.env["TESTNET_SUBTOPIA_CREATOR_MNEMONIC"] as string -); const creatorSignerAccount = transactionSignerAccount( makeBasicAccountTransactionSigner(creatorAccount), creatorAccount.addr ); -const bobTestAccount = mnemonicToSecretKey( - process.env["TESTNET_SUBTOPIA_BOB_MNEMONIC"] as string -); - const refundTestnetAlgos = async (account: Account) => { const accountInfo = await algodClient.accountInformation(account.addr).do(); @@ -65,6 +68,31 @@ const refundTestnetAlgos = async (account: Account) => { } }; +async function setupSubtopiaRegistryClient( + signerAccount: TransactionSignerAccount +) { + const subtopiaRegistryClient = await SubtopiaRegistryClient.init( + algodClient, + signerAccount, + ChainType.TESTNET + ); + + let lockerID = await SubtopiaRegistryClient.getLocker({ + registryID: subtopiaRegistryClient.appID, + algodClient: algodClient, + ownerAddress: creatorAccount.addr, + }); + if (!lockerID) { + const response = await subtopiaRegistryClient.createLocker({ + creator: signerAccount, + lockerType: LockerType.CREATOR, + }); + lockerID = response.lockerID; + } + + return { subtopiaRegistryClient, lockerID }; +} + describe("subtopia", () => { beforeAll(async () => { const dispenserInfo = await algodClient @@ -103,27 +131,11 @@ describe("subtopia", () => { }); it( - "should correctly add product, create subscription, delete subscription and delete product", + "should manage product and subscription lifecycle correctly", async () => { // Setup - const subtopiaRegistryClient = await SubtopiaRegistryClient.init( - algodClient, - creatorSignerAccount, - ChainType.TESTNET - ); - - let lockerID = await SubtopiaRegistryClient.getLocker({ - registryID: subtopiaRegistryClient.appID, - algodClient: algodClient, - ownerAddress: creatorAccount.addr, - }); - if (lockerID === undefined) { - const response = await subtopiaRegistryClient.createLocker({ - creator: creatorSignerAccount, - lockerType: LockerType.CREATOR, - }); - lockerID = response.lockerID; - } + const { subtopiaRegistryClient, lockerID } = + await setupSubtopiaRegistryClient(creatorSignerAccount); // Test const response = await subtopiaRegistryClient.createInfrastructure({ @@ -218,34 +230,30 @@ describe("subtopia", () => { // Assert expect(content.price.value).toBe(algos(1).microAlgos); + + const disableProductResponse = await productClient.disable(); + + expect(disableProductResponse.txID).toBeDefined(); + + const deleteProductResponse = + await subtopiaRegistryClient.deleteInfrastructure({ + infrastructureID: productClient.appID, + lockerID: lockerID, + }); + + expect(deleteProductResponse.txID).toBeDefined(); }, { timeout: 10e6 } ); it( - "should correctly add product, create discount, transfer product and delete discount", + "should handle product and discount operations correctly", async () => { - const subtopiaRegistryClient = await SubtopiaRegistryClient.init( - algodClient, - creatorSignerAccount, - ChainType.TESTNET - ); + const { subtopiaRegistryClient, lockerID } = + await setupSubtopiaRegistryClient(creatorSignerAccount); const newOwner = bobTestAccount; - let lockerID = await SubtopiaRegistryClient.getLocker({ - registryID: subtopiaRegistryClient.appID, - algodClient: algodClient, - ownerAddress: creatorAccount.addr, - }); - if (lockerID === undefined) { - const response = await subtopiaRegistryClient.createLocker({ - creator: creatorSignerAccount, - lockerType: LockerType.CREATOR, - }); - lockerID = response.lockerID; - } - // Test const response = await subtopiaRegistryClient.createInfrastructure({ productName: "Hooli", @@ -308,6 +316,35 @@ describe("subtopia", () => { ); expect(deleteDiscountResponse.txID).toBeDefined(); + + const disableProductResponse = await newOwnerProductClient.disable(); + + expect(disableProductResponse.txID).toBeDefined(); + + const newOwnerLockerID = await SubtopiaRegistryClient.getLocker({ + registryID: subtopiaRegistryClient.appID, + algodClient: algodClient, + ownerAddress: newOwner.addr, + }); + + expect(newOwnerLockerID).toBeGreaterThan(0); + + const newOwnerRegistryClient = await SubtopiaRegistryClient.init( + algodClient, + transactionSignerAccount( + makeBasicAccountTransactionSigner(newOwner), + newOwner.addr + ), + ChainType.TESTNET + ); + + const deleteProductResponse = + await newOwnerRegistryClient.deleteInfrastructure({ + infrastructureID: productClient.appID, + lockerID: newOwnerLockerID as number, + }); + + expect(deleteProductResponse.txID).toBeDefined(); }, { timeout: 10e6,