From 650a1bac88c4cbbceb9b934cd75140576dcc7be6 Mon Sep 17 00:00:00 2001 From: Simon Ingeson <44818+smonn@users.noreply.github.com> Date: Thu, 20 Jan 2022 20:23:03 -0500 Subject: [PATCH 1/5] Update AlgorandAdapter Move around how and when creator accounts are generated to avoid loss of funds. Also change defaultFrozen to be false. --- apps/api/src/lib/algorand-adapter.ts | 90 ++++++++++++++----- .../collectibles/collectibles.service.ts | 79 ++++++++-------- 2 files changed, 108 insertions(+), 61 deletions(-) diff --git a/apps/api/src/lib/algorand-adapter.ts b/apps/api/src/lib/algorand-adapter.ts index 7febb1c5e..55ecf4916 100644 --- a/apps/api/src/lib/algorand-adapter.ts +++ b/apps/api/src/lib/algorand-adapter.ts @@ -4,10 +4,11 @@ import algosdk from 'algosdk' import { Configuration } from '@/configuration' import { CollectibleModel } from '@/models/collectible.model' import { decrypt, encrypt } from '@/utils/encryption' +import { invariant } from '@/utils/invariant' import { logger } from '@/utils/logger' // 100_000 microAlgos = 0.1 ALGO -const DEFAULT_INITIAL_BALANCE = 100_000 +export const DEFAULT_INITIAL_BALANCE = 100_000 export interface PublicAccount { address: string @@ -23,6 +24,30 @@ export interface AlgorandAdapterOptions { fundingMnemonic: string } +export interface AccountInfo { + address: string + amount: number + + /* + "address": "MART2AF73ECUJ6WWZVQA5C6CCWSXKVDHALE273VMDAJUCODDEJEPMS6GOU", + "amount": 33672000, + "amount-without-pending-rewards": 33672000, + "apps-local-state": [], + "apps-total-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "assets": [], + "created-apps": [], + "created-assets": [], + "pending-rewards": 0, + "reward-base": 27521, + "rewards": 0, + "round": 19265963, + "status": "Offline" + */ +} + export default class AlgorandAdapter { logger = logger.child({ context: this.constructor.name }) fundingAccount: algosdk.Account @@ -36,13 +61,14 @@ export default class AlgorandAdapter { ) this.fundingAccount = algosdk.mnemonicToSecretKey(options.fundingMnemonic) + this.logger.info('Using funding account %s', this.fundingAccount.addr) this.testConnection() } async testConnection() { try { - const status = this.algod.status().do() + const status = await this.algod.status().do() this.logger.info({ status }, 'Successfully connected to Algod') } catch (error) { this.logger.error(error, 'Failed to connect to Algod') @@ -247,31 +273,49 @@ export default class AlgorandAdapter { await this.waitForConfirmation(transaction.txID()) } + async getAccountInfo(account: string): Promise { + const info = await this.algod.accountInformation(account).do() + return { + address: info.address, + amount: info.amount, + } + } + + async getCreatorAccount(initialBalance: number) { + const fundingAccountInfo = await this.getAccountInfo( + this.fundingAccount.addr + ) + + invariant( + fundingAccountInfo.amount > initialBalance + 100_000, + `Not enough funds on account ${fundingAccountInfo.address}. Have ${ + fundingAccountInfo.amount + } microAlgos, need ${initialBalance + 100_000} microAlgos.` + ) + + const creator = await this.createAccount( + Configuration.creatorPassphrase, + initialBalance + ) + + await this.submitTransaction(creator.signedTransactions) + + // Just need to wait for the funding transaction to complete + await this.waitForConfirmation(creator.transactionIds[0]) + + return creator + } + async generateCreateAssetTransactions( collectibles: CollectibleModel[], templates: CollectibleBase[], - useCreatorAccount?: boolean + creator?: PublicAccount ) { const suggestedParams = await this.algod.getTransactionParams().do() const templateLookup = new Map(templates.map((t) => [t.templateId, t])) let fromAccount = this.fundingAccount - let creator: PublicAccount | undefined - - if (useCreatorAccount) { - const initialBalance = - DEFAULT_INITIAL_BALANCE + - // 0.1 ALGO per collectible - collectibles.length * 100_000 + - // 1000 microAlgos per create transaction - collectibles.length * 1000 - - creator = await this.createAccount( - Configuration.creatorPassphrase, - initialBalance - ) - await this.submitTransaction(creator.signedTransactions) - // Just need to wait for the funding transaction to complete - await this.waitForConfirmation(creator.transactionIds[0]) + + if (creator) { fromAccount = algosdk.mnemonicToSecretKey( decrypt(creator.encryptedMnemonic, Configuration.creatorPassphrase) ) @@ -296,7 +340,7 @@ export default class AlgorandAdapter { from: fromAccount.addr, total: 1, decimals: 0, - defaultFrozen: true, + defaultFrozen: false, clawback: this.fundingAccount.addr, freeze: this.fundingAccount.addr, manager: this.fundingAccount.addr, @@ -317,7 +361,6 @@ export default class AlgorandAdapter { return { signedTransactions, transactionIds, - creator, } } @@ -351,8 +394,7 @@ export default class AlgorandAdapter { to: toAccount.addr, }) - // To avoid unfreezing accounts, transferring asset traditionally, and re-freezing the accounts - // for the asset, just use a clawback to "revoke" ownership from current owner to the buyer. + // Use a clawback to "revoke" ownership from current owner to the buyer. const clawbackTxn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ suggestedParams, diff --git a/apps/api/src/modules/collectibles/collectibles.service.ts b/apps/api/src/modules/collectibles/collectibles.service.ts index 6a4e77e8c..8ab6a205d 100644 --- a/apps/api/src/modules/collectibles/collectibles.service.ts +++ b/apps/api/src/modules/collectibles/collectibles.service.ts @@ -17,7 +17,9 @@ import { CollectibleShowcaseQuerystring } from '@algomart/schemas' import { Transaction } from 'objection' import AlgoExplorerAdapter from '@/lib/algoexplorer-adapter' -import AlgorandAdapter from '@/lib/algorand-adapter' +import AlgorandAdapter, { + DEFAULT_INITIAL_BALANCE, +} from '@/lib/algorand-adapter' import DirectusAdapter, { ItemFilter } from '@/lib/directus-adapter' import NFTStorageAdapter from '@/lib/nft-storage-adapter' import { AlgorandAccountModel } from '@/models/algorand-account.model' @@ -307,15 +309,49 @@ export default class CollectiblesService { invariant(templates.length > 0, 'templates not found') - // TODO load creator account from pool or always create a new one...? - // Hard-coded to be true until 1000 asset limit is removed - const useCreatorAccount = true + // TODO: remove the creator account once the 1000 asset limit is removed + const initialBalance = + DEFAULT_INITIAL_BALANCE + + // 0.1 ALGO per collectible + collectibles.length * 100_000 + + // 1000 microAlgos per create transaction + collectibles.length * 1000 + const creator = await this.algorand.getCreatorAccount(initialBalance) - const { signedTransactions, transactionIds, creator } = + const transactions = await AlgorandTransactionModel.query(trx).insert([ + { + // funding transaction + address: creator.transactionIds[0], + // Creator must already be confirmed for us to get here + status: AlgorandTransactionStatus.Confirmed, + }, + { + // non-participation transaction + address: creator.transactionIds[1], + status: AlgorandTransactionStatus.Pending, + }, + ]) + + const creatorAccount = await AlgorandAccountModel.query(trx).insertGraph( + { + address: creator.address, + encryptedKey: creator.encryptedMnemonic, + creationTransactionId: transactions[0].id, + }, + { relate: true } + ) + + await EventModel.query(trx).insert({ + action: EventAction.Create, + entityType: EventEntityType.AlgorandAccount, + entityId: creatorAccount.id, + }) + + const { signedTransactions, transactionIds } = await this.algorand.generateCreateAssetTransactions( collectibles, templates, - useCreatorAccount + creator ) this.logger.info('Using creator account %s', creator?.address || '-') @@ -330,37 +366,6 @@ export default class CollectiblesService { throw error } - if (creator) { - const transactions = await AlgorandTransactionModel.query(trx).insert([ - { - // funding transaction - address: creator.transactionIds[0], - // Creator must already be confirmed for us to get here - status: AlgorandTransactionStatus.Confirmed, - }, - { - // non-participation transaction - address: creator.transactionIds[1], - status: AlgorandTransactionStatus.Pending, - }, - ]) - - const creatorAccount = await AlgorandAccountModel.query(trx).insertGraph( - { - address: creator.address, - encryptedKey: creator.encryptedMnemonic, - creationTransactionId: transactions[0].id, - }, - { relate: true } - ) - - await EventModel.query(trx).insert({ - action: EventAction.Create, - entityType: EventEntityType.AlgorandAccount, - entityId: creatorAccount.id, - }) - } - await Promise.all( collectibles.map(async (collectible, index) => { return await CollectibleModel.query(trx).upsertGraph( From 4c67facfeb21cb4c1a23f469627d92fa45a42e8c Mon Sep 17 00:00:00 2001 From: Simon Ingeson <44818+smonn@users.noreply.github.com> Date: Thu, 20 Jan 2022 20:24:41 -0500 Subject: [PATCH 2/5] remove comments --- apps/api/src/lib/algorand-adapter.ts | 41 ---------------------------- 1 file changed, 41 deletions(-) diff --git a/apps/api/src/lib/algorand-adapter.ts b/apps/api/src/lib/algorand-adapter.ts index 55ecf4916..30cf7a145 100644 --- a/apps/api/src/lib/algorand-adapter.ts +++ b/apps/api/src/lib/algorand-adapter.ts @@ -27,25 +27,6 @@ export interface AlgorandAdapterOptions { export interface AccountInfo { address: string amount: number - - /* - "address": "MART2AF73ECUJ6WWZVQA5C6CCWSXKVDHALE273VMDAJUCODDEJEPMS6GOU", - "amount": 33672000, - "amount-without-pending-rewards": 33672000, - "apps-local-state": [], - "apps-total-schema": { - "num-byte-slice": 0, - "num-uint": 0 - }, - "assets": [], - "created-apps": [], - "created-assets": [], - "pending-rewards": 0, - "reward-base": 27521, - "rewards": 0, - "round": 19265963, - "status": "Offline" - */ } export default class AlgorandAdapter { @@ -174,28 +155,6 @@ export default class AlgorandAdapter { return null } - /* - { - "index": 269, - "params": { - "clawback": "GJM6OZHTWHSHBOQPBQNXDSMXLPUPTUE6VYRBTO24CEHWRQ2JX3NYCAHMI4", - "creator": "ADFI6NIG7FXBHSCSHHMHZIEFVKGPEDQ2QIL35TJZD3PGYAZBYZBOMDCX4E", - "decimals": 0, - "default-frozen": true, - "freeze": "GJM6OZHTWHSHBOQPBQNXDSMXLPUPTUE6VYRBTO24CEHWRQ2JX3NYCAHMI4", - "manager": "GJM6OZHTWHSHBOQPBQNXDSMXLPUPTUE6VYRBTO24CEHWRQ2JX3NYCAHMI4", - "name": "asset26 2/5", - "name-b64": "YXNzZXQyNiAyLzU=", - "reserve": "GJM6OZHTWHSHBOQPBQNXDSMXLPUPTUE6VYRBTO24CEHWRQ2JX3NYCAHMI4", - "total": 1, - "unit-name": "asset26", - "unit-name-b64": "YXNzZXQyNg==", - "url": "4ecb78d3-cd2e-4272-a6cb-9dedf9a86ed8", - "url-b64": "NGVjYjc4ZDMtY2QyZS00MjcyLWE2Y2ItOWRlZGY5YTg2ZWQ4" - } - } - */ - return { address: info.index as number, creator: info.params.creator as string, From 79169b4d34641419abab3ba1b3a3a094f8bdaeb0 Mon Sep 17 00:00:00 2001 From: Simon Ingeson <44818+smonn@users.noreply.github.com> Date: Fri, 21 Jan 2022 09:26:51 -0500 Subject: [PATCH 3/5] Disallow minting more than 16 collectibles Up to 16 transactions can be grouped together --- apps/api/src/lib/algorand-adapter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/api/src/lib/algorand-adapter.ts b/apps/api/src/lib/algorand-adapter.ts index 30cf7a145..b66a53cc0 100644 --- a/apps/api/src/lib/algorand-adapter.ts +++ b/apps/api/src/lib/algorand-adapter.ts @@ -270,6 +270,11 @@ export default class AlgorandAdapter { templates: CollectibleBase[], creator?: PublicAccount ) { + invariant( + collectibles.length <= 16, + 'Can only mint up to 16 assets at a time' + ) + const suggestedParams = await this.algod.getTransactionParams().do() const templateLookup = new Map(templates.map((t) => [t.templateId, t])) let fromAccount = this.fundingAccount From 4222a60ab0f61bde5cddc6137e131799aa0abf0d Mon Sep 17 00:00:00 2001 From: Simon Ingeson <44818+smonn@users.noreply.github.com> Date: Fri, 21 Jan 2022 09:27:30 -0500 Subject: [PATCH 4/5] Adhere to the 16 limit --- apps/api/src/modules/collectibles/collectibles.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/modules/collectibles/collectibles.service.ts b/apps/api/src/modules/collectibles/collectibles.service.ts index 8ab6a205d..c8e897a8e 100644 --- a/apps/api/src/modules/collectibles/collectibles.service.ts +++ b/apps/api/src/modules/collectibles/collectibles.service.ts @@ -292,6 +292,7 @@ export default class CollectiblesService { .whereNull('creationTransactionId') .joinRelated('pack', { alias: 'p' }) .whereNotNull('p.ownerId') + .limit(16) // No collectibles matched the IDs or they are all already minted if (collectibles.length === 0) return 0 From f77cfdc0cc48b076bce4e4535c4ac80903e95a84 Mon Sep 17 00:00:00 2001 From: Simon Ingeson <44818+smonn@users.noreply.github.com> Date: Fri, 21 Jan 2022 10:32:23 -0500 Subject: [PATCH 5/5] Bump up max db connections to 20 --- apps/api/src/configuration/knex-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/configuration/knex-config.ts b/apps/api/src/configuration/knex-config.ts index f731f13a6..d6354cf75 100644 --- a/apps/api/src/configuration/knex-config.ts +++ b/apps/api/src/configuration/knex-config.ts @@ -8,6 +8,7 @@ export default function buildKnexConfiguration(): Knex.Config { client: 'pg', connection: Configuration.databaseUrl, searchPath: [Configuration.databaseSchema], + pool: { min: 2, max: 20 }, migrations: { extension: 'ts', directory: path.join(__dirname, '..', 'migrations'),