From 7713060921df2daf310b9583011c2bbcb040374e Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 11:39:13 +0100 Subject: [PATCH 01/28] add marmalade generate contract script --- packages/libs/client-utils/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/libs/client-utils/package.json b/packages/libs/client-utils/package.json index 5952ebcd5f..b3fbb48861 100644 --- a/packages/libs/client-utils/package.json +++ b/packages/libs/client-utils/package.json @@ -63,7 +63,9 @@ "lint:fmt": "prettier . --cache --check", "lint:pkg": "lint-package", "lint:src": "eslint src --ext .js,.ts", - "pactjs:generate:contract": "pactjs contract-generate --contract coin --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact", + "pactjs:generate:contract": "pnpm run pactjs:generate:contract:coin && pnpm run pactjs:generate:contract:marmalade", + "pactjs:generate:contract:coin": "pactjs contract-generate --contract coin --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact", + "pactjs:generate:contract:marmalade": "pactjs contract-generate --contract marmalade-v2.ledger --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact;", "test": "vitest run", "test:integration": "vitest run -c ./vitest.integration.config.ts", "test:watch": "vitest" From 3ecf4ef56309bf824c01a4cc1961a03a19ac8311 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 11:48:35 +0100 Subject: [PATCH 02/28] create token and create token id functions --- .../src/marmalade/create-token-id.ts | 53 ++++++++++++++++ .../src/marmalade/create-token.ts | 61 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/libs/client-utils/src/marmalade/create-token-id.ts create mode 100644 packages/libs/client-utils/src/marmalade/create-token.ts diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts new file mode 100644 index 0000000000..f237f7c444 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -0,0 +1,53 @@ +import { ChainId, Pact, readKeyset } from '@kadena/client'; +import { + addKeyset, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import { dirtyReadClient } from '../core/client-helpers'; +import { IClientConfig } from '../core/utils/helpers'; + +interface ICreateTokenIdInput { + policies?: string[]; + uri: string; + precision: number; + chainId: ChainId; + creator: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + networkId: string; + host: IClientConfig['host']; +} + +export async function createTokenId({ + policies = [], + uri, + precision, + creator, + host, + networkId, + chainId, +}: ICreateTokenIdInput): Promise { + return (await dirtyReadClient({ + host, + defaults: { + networkId, + }, + })( + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['create-token-id']( + { precision, uri, policies }, + readKeyset('creation-guard'), + ), + ), + addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), + setMeta({ senderAccount: creator.account, chainId }), + ), + ).execute()) as string; +} diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts new file mode 100644 index 0000000000..2e2ba09603 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -0,0 +1,61 @@ +import type { ChainId, ICommandResult, PactReference } from '@kadena/client'; +import { Pact, createSignWithKeypair, readKeyset } from '@kadena/client'; +import { + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import { PactNumber } from '@kadena/pactjs'; +import { submitClient } from '../core/client-helpers'; +import { IClientConfig } from '../core/utils/helpers'; + +interface ICreateTokenInput { + policies?: string[]; + uri: string; + tokenId: string; + precision: number; + chainId: ChainId; + creator: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +export const createTokenCommand = ({ + policies = [], + uri, + tokenId, + precision, + creator, + chainId, +}: ICreateTokenInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['create-token']( + tokenId, + new PactNumber(precision).toPactInteger(), + uri, + policies.length > 0 + ? ([policies.join(' ')] as unknown as PactReference) + : ([] as unknown as PactReference), + readKeyset('creation-guard'), + ), + ), + addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), + addSigner(creator.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor('marmalade-v2.ledger.CREATE-TOKEN', tokenId, { + pred: creator.keyset.pred, + keys: creator.keyset.keys, + }), + ]), + setMeta({ senderAccount: creator.account, chainId }), + ); + +export const createToken = (inputs: ICreateTokenInput, config: IClientConfig) => + submitClient(config)(createTokenCommand(inputs)); From f2baef53a3faa8d1b418d56a007d3ebff9ab8241 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 14:21:38 +0100 Subject: [PATCH 03/28] small refactor to create token id and create token --- .../src/marmalade/create-token-id.ts | 48 ++++++++++--------- .../src/marmalade/create-token.ts | 18 ++++++- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index f237f7c444..82a2230f64 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -1,4 +1,10 @@ -import { ChainId, Pact, readKeyset } from '@kadena/client'; +import { + ChainId, + IPactModules, + Pact, + PactReturnType, + readKeyset, +} from '@kadena/client'; import { addKeyset, composePactCommand, @@ -20,34 +26,30 @@ interface ICreateTokenIdInput { pred: 'keys-all' | 'keys-2' | 'keys-any'; }; }; - networkId: string; - host: IClientConfig['host']; } -export async function createTokenId({ +export const createTokenIdCommand = ({ policies = [], uri, precision, creator, - host, - networkId, chainId, -}: ICreateTokenIdInput): Promise { - return (await dirtyReadClient({ - host, - defaults: { - networkId, - }, - })( - composePactCommand( - execution( - Pact.modules['marmalade-v2.ledger']['create-token-id']( - { precision, uri, policies }, - readKeyset('creation-guard'), - ), +}: ICreateTokenIdInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['create-token-id']( + { precision, uri, policies }, + readKeyset('creation-guard'), ), - addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), - setMeta({ senderAccount: creator.account, chainId }), ), - ).execute()) as string; -} + addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), + setMeta({ senderAccount: creator.account, chainId }), + ); + +export const createTokenId = ( + inputs: ICreateTokenIdInput, + config: IClientConfig, +) => + dirtyReadClient< + PactReturnType + >(config)(createTokenIdCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 2e2ba09603..063285958f 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -1,5 +1,11 @@ -import type { ChainId, ICommandResult, PactReference } from '@kadena/client'; -import { Pact, createSignWithKeypair, readKeyset } from '@kadena/client'; +import type { + ChainId, + ICommandResult, + IPactModules, + PactReference, + PactReturnType, +} from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; import { addKeyset, addSigner, @@ -59,3 +65,11 @@ export const createTokenCommand = ({ export const createToken = (inputs: ICreateTokenInput, config: IClientConfig) => submitClient(config)(createTokenCommand(inputs)); + +export const createToken1 = ( + inputs: ICreateTokenInput, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(createTokenCommand(inputs)); From fc7b5e326e72c494d551db8c6aa2351d385a2068 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 16:56:02 +0100 Subject: [PATCH 04/28] mint token functionality --- .../client-utils/src/marmalade/mint-token.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/libs/client-utils/src/marmalade/mint-token.ts diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts new file mode 100644 index 0000000000..7785e87781 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -0,0 +1,54 @@ +import { IPactModules, Pact, PactReturnType, readKeyset } from '@kadena/client'; +import { + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import { ChainId, IPactDecimal } from '@kadena/types'; +import { submitClient } from '../core'; +import { IClientConfig } from '../core/utils/helpers'; + +export interface IMintTokenInput { + tokenId: string; + creatorAccount: string; + chainId: ChainId; + guard: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + amount: IPactDecimal; +} + +export const mintTokenCommand = ({ + tokenId, + creatorAccount, + chainId, + guard, + amount, +}: IMintTokenInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger'].mint( + tokenId, + creatorAccount, + readKeyset('guard'), + amount, + ), + ), + addKeyset('guard', guard.keyset.pred, ...guard.keyset.keys), + addSigner(guard.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor('marmalade-v2.ledger.MINT', tokenId, creatorAccount, amount), + ]), + setMeta({ senderAccount: guard.account, chainId }), + ); + +export const mintToken = (inputs: IMintTokenInput, config: IClientConfig) => + submitClient>( + config, + )(mintTokenCommand(inputs)); From 27502169efcf088bdfb3741fbc5e828b2ffb2ce5 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 16:56:12 +0100 Subject: [PATCH 05/28] transfer-create function --- .../src/marmalade/transfer-create-token.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/libs/client-utils/src/marmalade/transfer-create-token.ts diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts new file mode 100644 index 0000000000..319fec721c --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -0,0 +1,70 @@ +import { IPactModules, Pact, PactReturnType, readKeyset } from '@kadena/client'; +import { + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import { ChainId, IPactDecimal } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import { IClientConfig } from '../core/utils/helpers'; + +export interface ITransferCreateTokenInput { + tokenId: string; + chainId: ChainId; + sender: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + receiver: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + amount: IPactDecimal; +} + +export const createTransferTokenCommand = ({ + tokenId, + chainId, + sender, + receiver, + amount, +}: ITransferCreateTokenInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['transfer-create']( + tokenId, + sender.account, + receiver.account, + readKeyset('receiver-guard'), + amount, + ), + ), + addKeyset('receiver-guard', receiver.keyset.pred, ...receiver.keyset.keys), + addSigner(sender.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + 'marmalade-v2.ledger.TRANSFER', + tokenId, + sender.account, + receiver.account, + amount, + ), + ]), + setMeta({ senderAccount: sender.account, chainId }), + ); + +export const transferCreateToken = ( + inputs: ITransferCreateTokenInput, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(createTransferTokenCommand(inputs)); From 12a74f18ab4a775958443a71326c040b07041603 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 16:56:19 +0100 Subject: [PATCH 06/28] get balance function --- .../client-utils/src/marmalade/get-balance.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/libs/client-utils/src/marmalade/get-balance.ts diff --git a/packages/libs/client-utils/src/marmalade/get-balance.ts b/packages/libs/client-utils/src/marmalade/get-balance.ts new file mode 100644 index 0000000000..cb3cc2b10d --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-balance.ts @@ -0,0 +1,50 @@ +import { IPactModules, Pact, PactReturnType } from '@kadena/client'; +import { + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import { ChainId } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import { IClientConfig } from '../core/utils/helpers'; + +interface IGeBalanceInput { + tokenId: string; + chainId: ChainId; + accountName: string; + guard: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +export const getBalanceCommand = ({ + tokenId, + chainId, + accountName, + guard, +}: IGeBalanceInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['get-balance'](tokenId, accountName), + ), + addKeyset('guard', 'keys-all', ...guard.keyset.keys), + addSigner(guard.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor('marmalade-v2.ledger.get-balance', tokenId, accountName), + ]), + setMeta({ + senderAccount: guard.account, + chainId, + }), + ); + +export const getBalance = (inputs: IGeBalanceInput, config: IClientConfig) => + submitClient< + PactReturnType + >(config)(getBalanceCommand(inputs)); From 8e0eba414d0774c43c722a1e584f63dfe6f0a099 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 16 Jan 2024 16:58:24 +0100 Subject: [PATCH 07/28] refactor createToken --- packages/libs/client-utils/src/marmalade/create-token.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 063285958f..51969046df 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -1,6 +1,5 @@ import type { ChainId, - ICommandResult, IPactModules, PactReference, PactReturnType, @@ -64,12 +63,6 @@ export const createTokenCommand = ({ ); export const createToken = (inputs: ICreateTokenInput, config: IClientConfig) => - submitClient(config)(createTokenCommand(inputs)); - -export const createToken1 = ( - inputs: ICreateTokenInput, - config: IClientConfig, -) => submitClient< PactReturnType >(config)(createTokenCommand(inputs)); From 8df7ac0568ca67f379947efa7f47d2411cdf0326 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 17 Jan 2024 09:34:57 +0100 Subject: [PATCH 08/28] index file --- .../libs/client-utils/src/marmalade/index.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/libs/client-utils/src/marmalade/index.ts diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts new file mode 100644 index 0000000000..1521e5a160 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -0,0 +1,48 @@ +import { createSignWithKeypair } from '@kadena/client'; +import { ChainId } from '@kadena/types'; +import { IClientConfig } from '../core/utils/helpers'; +import { createToken } from './create-token'; +import { createTokenId } from './create-token-id'; + +console.log('marmalade'); + +const input = { + uri: Date.now().toString(), + precision: 0, + chainId: '0' as ChainId, + creator: { + account: 'sender00', + keyset: { + pred: 'keys-all' as const, + keys: [ + '368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca', + ], + }, + }, +}; + +const config: IClientConfig = { + host: 'http://localhost:8080', + defaults: { + networkId: 'fast-development', + }, + sign: createSignWithKeypair({ + publicKey: + '368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca', + secretKey: + '251a920c403ae8c8f65f59142316af3c82b631fba46ddea92ee8c95035bd2898', + }), +}; + +const tokenId = createTokenId(input, config).execute(); +tokenId.then((res) => { + console.log(res); + if (res === undefined) return; + const input2 = { ...input, tokenId: res }; + + const tokenCreate = createToken(input2, config).executeTo('listen'); + + tokenCreate.then((res: any) => { + console.log(res); + }); +}); From 76d55026c769c3bdbc9e6500bb2ceff56ca3188d Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Fri, 19 Jan 2024 16:58:55 +0100 Subject: [PATCH 09/28] added unit testing for createId, create and mint tokens --- .../integration-tests/marmalade.int.test.ts | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts new file mode 100644 index 0000000000..27da9dd49c --- /dev/null +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -0,0 +1,189 @@ +import { ChainId, createSignWithKeypair } from '@kadena/client'; +import { PactNumber } from '@kadena/pactjs'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { transferCreate } from '../coin'; +import { createToken } from '../marmalade/create-token'; +import { createTokenId } from '../marmalade/create-token-id'; +import { mintToken } from '../marmalade/mint-token'; +import { NetworkIds } from './support/NetworkIds'; +import { withStepFactory } from './support/helpers'; +import { sender00Account, sourceAccount } from './test-data/accounts'; + +let tokenId: string | undefined; +let inputs = { + chainId: '0' as ChainId, + precision: 0, + uri: Date.now().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, +}; +let config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'fast-development', + }, + sign: createSignWithKeypair([sourceAccount]), +}; + +beforeAll(async () => { + const result = await transferCreate( + { + sender: { + account: sender00Account.account, + publicKeys: [sender00Account.publicKey], + }, + receiver: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all', + }, + }, + amount: '100', + chainId: '0', + }, + { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'fast-development', + }, + sign: createSignWithKeypair([sender00Account]), + }, + ).execute(); + + expect(result).toBe('Write succeeded'); +}); + +describe('createTokenId', () => { + it('should return a token id', async () => { + tokenId = await createTokenId(inputs, config).execute(); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); +}); + +describe('createToken', () => { + it('shoud create a token', async () => { + const withStep = withStepFactory(); + + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.fast_development); + expect(trDesc.chainId).toBe('0'); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('mintToken', () => { + it('shoud mint a token', async () => { + const withStep = withStepFactory(); + + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + creatorAccount: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.fast_development); + expect(trDesc.chainId).toBe('0'); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + console.log(sbResult); + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); From 3f98192a682083841c307ff5817f60b991b01d1c Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Mon, 22 Jan 2024 16:20:30 +0100 Subject: [PATCH 10/28] integration testing; lint and style fixes; index file for marmalade --- .../integration-tests/marmalade.int.test.ts | 288 ++++++++++++++++-- .../integration-tests/test-data/accounts.ts | 8 + .../src/marmalade/create-token-id.ts | 11 +- .../src/marmalade/create-token.ts | 2 +- .../client-utils/src/marmalade/get-balance.ts | 7 +- .../libs/client-utils/src/marmalade/index.ts | 53 +--- .../client-utils/src/marmalade/mint-token.ts | 7 +- .../src/marmalade/transfer-create-token.ts | 7 +- 8 files changed, 289 insertions(+), 94 deletions(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 27da9dd49c..c267493045 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -1,16 +1,23 @@ -import { ChainId, createSignWithKeypair } from '@kadena/client'; +import type { ChainId } from '@kadena/client'; +import { createSignWithKeypair } from '@kadena/client'; import { PactNumber } from '@kadena/pactjs'; import { beforeAll, describe, expect, it } from 'vitest'; import { transferCreate } from '../coin'; import { createToken } from '../marmalade/create-token'; import { createTokenId } from '../marmalade/create-token-id'; +import { getBalance } from '../marmalade/get-balance'; import { mintToken } from '../marmalade/mint-token'; +import { transferCreateToken } from '../marmalade/transfer-create-token'; import { NetworkIds } from './support/NetworkIds'; import { withStepFactory } from './support/helpers'; -import { sender00Account, sourceAccount } from './test-data/accounts'; +import { + secondaryTargetAccount, + sender00Account, + sourceAccount, +} from './test-data/accounts'; let tokenId: string | undefined; -let inputs = { +const inputs = { chainId: '0' as ChainId, precision: 0, uri: Date.now().toString(), @@ -23,7 +30,7 @@ let inputs = { }, }, }; -let config = { +const config = { host: 'http://127.0.0.1:8080', defaults: { networkId: 'fast-development', @@ -32,33 +39,55 @@ let config = { }; beforeAll(async () => { - const result = await transferCreate( - { - sender: { - account: sender00Account.account, - publicKeys: [sender00Account.publicKey], - }, - receiver: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all', + const fundConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'fast-development', + }, + sign: createSignWithKeypair([sender00Account]), + }; + const [resultSourceAccount, resultTargetAccount] = await Promise.all([ + transferCreate( + { + sender: { + account: sender00Account.account, + publicKeys: [sender00Account.publicKey], }, + receiver: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all', + }, + }, + amount: '100', + chainId: '0', }, - amount: '100', - chainId: '0', - }, - { - host: 'http://127.0.0.1:8080', - defaults: { - networkId: 'fast-development', + fundConfig, + ).execute(), + transferCreate( + { + sender: { + account: sender00Account.account, + publicKeys: [sender00Account.publicKey], + }, + receiver: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all', + }, + }, + amount: '100', + chainId: '0', }, - sign: createSignWithKeypair([sender00Account]), - }, - ).execute(); + fundConfig, + ).execute(), + ]); - expect(result).toBe('Write succeeded'); -}); + expect(resultSourceAccount).toBe('Write succeeded'); + expect(resultTargetAccount).toBe('Write succeeded'); +}, 30000); describe('createTokenId', () => { it('should return a token id', async () => { @@ -173,7 +202,6 @@ describe('mintToken', () => { .on( 'listen', withStep((step, sbResult) => { - console.log(sbResult); expect(step).toBe(4); if (sbResult.result.status === 'failure') { expect(sbResult.result.status).toBe('success'); @@ -185,5 +213,209 @@ describe('mintToken', () => { .execute(); expect(result).toBe(true); + + const balance = await getBalance( + { + accountName: sourceAccount.account, + chainId: '0', + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + config, + ).execute(); + + expect(balance).toBe(1); + }); + it('shoud throw error when non-existent token is minted', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = mintToken( + { + ...inputs, + tokenId: nonExistingTokenId, + creatorAccount: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('with-read: row not found: non-existing-token'), + ); + }); +}); + +describe('getBalance', () => { + it('shoud get a balance', async () => { + const result = await getBalance( + { + accountName: sourceAccount.account, + chainId: '0', + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + config, + ).execute(); + + expect(result).toBeGreaterThan(0); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = getBalance( + { + accountName: sourceAccount.account, + chainId: '0', + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: nonExistingTokenId, + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error( + `read: row not found: ${nonExistingTokenId}:${sourceAccount.account}`, + ), + ); + }); +}); + +describe('transferCreateToken', () => { + it('shoud transfer-create a token', async () => { + const withStep = withStepFactory(); + + const result = await transferCreateToken( + { + ...inputs, + tokenId: tokenId as string, + sender: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + receiver: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.fast_development); + expect(trDesc.chainId).toBe('0'); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getBalance( + { + accountName: secondaryTargetAccount.account, + chainId: '0', + guard: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + { ...config, sign: createSignWithKeypair([secondaryTargetAccount]) }, + ).execute(); + + expect(balance).toBe(1); + }); + it('should throw an error if source account does not contain token', async () => { + const task = transferCreateToken( + { + ...inputs, + tokenId: tokenId as string, + sender: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + receiver: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(100).toPactDecimal(), + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('Insufficient funds'), + ); }); }); diff --git a/packages/libs/client-utils/src/integration-tests/test-data/accounts.ts b/packages/libs/client-utils/src/integration-tests/test-data/accounts.ts index cf23a0cf9d..e0c6da37a3 100644 --- a/packages/libs/client-utils/src/integration-tests/test-data/accounts.ts +++ b/packages/libs/client-utils/src/integration-tests/test-data/accounts.ts @@ -25,3 +25,11 @@ export const targetAccount: IAccount = { chainId: '1', guard: '5a2afbc4564b76b2c27ce5a644cab643c43663835ea0be22433b209d3351f937', }; +export const secondaryTargetAccount: IAccountWithSecretKey = { + account: 'k:5f6f02565cf70fe29a43eb251028673ceb43263707a2d6eee929a97690223b87', + publicKey: '5f6f02565cf70fe29a43eb251028673ceb43263707a2d6eee929a97690223b87', + chainId: '0', + guard: '5f6f02565cf70fe29a43eb251028673ceb43263707a2d6eee929a97690223b87', + // this is here only for testing purposes. in a real world scenario, the secret key should never be exposed + secretKey: 'a3c949df6ff96f4b82a9c7f2e44f5adccaf0e4278eef2000164383912b6fd8b4', +}; diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index 82a2230f64..2b306e5870 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -1,10 +1,5 @@ -import { - ChainId, - IPactModules, - Pact, - PactReturnType, - readKeyset, -} from '@kadena/client'; +import type { ChainId, IPactModules, PactReturnType } from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; import { addKeyset, composePactCommand, @@ -12,7 +7,7 @@ import { setMeta, } from '@kadena/client/fp'; import { dirtyReadClient } from '../core/client-helpers'; -import { IClientConfig } from '../core/utils/helpers'; +import type { IClientConfig } from '../core/utils/helpers'; interface ICreateTokenIdInput { policies?: string[]; diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 51969046df..05200bb656 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -14,7 +14,7 @@ import { } from '@kadena/client/fp'; import { PactNumber } from '@kadena/pactjs'; import { submitClient } from '../core/client-helpers'; -import { IClientConfig } from '../core/utils/helpers'; +import type { IClientConfig } from '../core/utils/helpers'; interface ICreateTokenInput { policies?: string[]; diff --git a/packages/libs/client-utils/src/marmalade/get-balance.ts b/packages/libs/client-utils/src/marmalade/get-balance.ts index cb3cc2b10d..38c0de4d4d 100644 --- a/packages/libs/client-utils/src/marmalade/get-balance.ts +++ b/packages/libs/client-utils/src/marmalade/get-balance.ts @@ -1,4 +1,5 @@ -import { IPactModules, Pact, PactReturnType } from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; import { addKeyset, addSigner, @@ -6,9 +7,9 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import { ChainId } from '@kadena/types'; +import type { ChainId } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; -import { IClientConfig } from '../core/utils/helpers'; +import type { IClientConfig } from '../core/utils/helpers'; interface IGeBalanceInput { tokenId: string; diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index 1521e5a160..25db8e5162 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -1,48 +1,5 @@ -import { createSignWithKeypair } from '@kadena/client'; -import { ChainId } from '@kadena/types'; -import { IClientConfig } from '../core/utils/helpers'; -import { createToken } from './create-token'; -import { createTokenId } from './create-token-id'; - -console.log('marmalade'); - -const input = { - uri: Date.now().toString(), - precision: 0, - chainId: '0' as ChainId, - creator: { - account: 'sender00', - keyset: { - pred: 'keys-all' as const, - keys: [ - '368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca', - ], - }, - }, -}; - -const config: IClientConfig = { - host: 'http://localhost:8080', - defaults: { - networkId: 'fast-development', - }, - sign: createSignWithKeypair({ - publicKey: - '368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca', - secretKey: - '251a920c403ae8c8f65f59142316af3c82b631fba46ddea92ee8c95035bd2898', - }), -}; - -const tokenId = createTokenId(input, config).execute(); -tokenId.then((res) => { - console.log(res); - if (res === undefined) return; - const input2 = { ...input, tokenId: res }; - - const tokenCreate = createToken(input2, config).executeTo('listen'); - - tokenCreate.then((res: any) => { - console.log(res); - }); -}); +export * from './create-token'; +export * from './create-token-id'; +export * from './get-balance'; +export * from './mint-token'; +export * from './transfer-create-token'; diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts index 7785e87781..bdd7e53cf8 100644 --- a/packages/libs/client-utils/src/marmalade/mint-token.ts +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -1,4 +1,5 @@ -import { IPactModules, Pact, PactReturnType, readKeyset } from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; import { addKeyset, addSigner, @@ -6,9 +7,9 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import { ChainId, IPactDecimal } from '@kadena/types'; +import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; -import { IClientConfig } from '../core/utils/helpers'; +import type { IClientConfig } from '../core/utils/helpers'; export interface IMintTokenInput { tokenId: string; diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts index 319fec721c..40cb084827 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -1,4 +1,5 @@ -import { IPactModules, Pact, PactReturnType, readKeyset } from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; import { addKeyset, addSigner, @@ -6,9 +7,9 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import { ChainId, IPactDecimal } from '@kadena/types'; +import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; -import { IClientConfig } from '../core/utils/helpers'; +import type { IClientConfig } from '../core/utils/helpers'; export interface ITransferCreateTokenInput { tokenId: string; From 5c503e0bd0dcae1579761629a411540ae34b504c Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Mon, 22 Jan 2024 16:25:22 +0100 Subject: [PATCH 11/28] removed unecessary exports --- packages/libs/client-utils/src/marmalade/create-token-id.ts | 2 +- packages/libs/client-utils/src/marmalade/create-token.ts | 2 +- packages/libs/client-utils/src/marmalade/get-balance.ts | 2 +- packages/libs/client-utils/src/marmalade/mint-token.ts | 4 ++-- .../libs/client-utils/src/marmalade/transfer-create-token.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index 2b306e5870..58405b49ca 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -23,7 +23,7 @@ interface ICreateTokenIdInput { }; } -export const createTokenIdCommand = ({ +const createTokenIdCommand = ({ policies = [], uri, precision, diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 05200bb656..3c15877bb1 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -31,7 +31,7 @@ interface ICreateTokenInput { }; } -export const createTokenCommand = ({ +const createTokenCommand = ({ policies = [], uri, tokenId, diff --git a/packages/libs/client-utils/src/marmalade/get-balance.ts b/packages/libs/client-utils/src/marmalade/get-balance.ts index 38c0de4d4d..80a52d7576 100644 --- a/packages/libs/client-utils/src/marmalade/get-balance.ts +++ b/packages/libs/client-utils/src/marmalade/get-balance.ts @@ -24,7 +24,7 @@ interface IGeBalanceInput { }; } -export const getBalanceCommand = ({ +const getBalanceCommand = ({ tokenId, chainId, accountName, diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts index bdd7e53cf8..0ebf4e7043 100644 --- a/packages/libs/client-utils/src/marmalade/mint-token.ts +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -11,7 +11,7 @@ import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -export interface IMintTokenInput { +interface IMintTokenInput { tokenId: string; creatorAccount: string; chainId: ChainId; @@ -25,7 +25,7 @@ export interface IMintTokenInput { amount: IPactDecimal; } -export const mintTokenCommand = ({ +const mintTokenCommand = ({ tokenId, creatorAccount, chainId, diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts index 40cb084827..977187db76 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -11,7 +11,7 @@ import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; -export interface ITransferCreateTokenInput { +interface ITransferCreateTokenInput { tokenId: string; chainId: ChainId; sender: { @@ -31,7 +31,7 @@ export interface ITransferCreateTokenInput { amount: IPactDecimal; } -export const createTransferTokenCommand = ({ +const createTransferTokenCommand = ({ tokenId, chainId, sender, From a53cae647e2aaae3c5c9dd47276ab667b94b3cc8 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Mon, 22 Jan 2024 16:26:42 +0100 Subject: [PATCH 12/28] omit sign in dirty reads --- packages/libs/client-utils/src/marmalade/create-token-id.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index 58405b49ca..bf3ffc6952 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -43,7 +43,7 @@ const createTokenIdCommand = ({ export const createTokenId = ( inputs: ICreateTokenIdInput, - config: IClientConfig, + config: Omit, ) => dirtyReadClient< PactReturnType From e09d96b4b4663fdc09519f77a897efdc82c68b0a Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Mon, 22 Jan 2024 16:40:41 +0100 Subject: [PATCH 13/28] changeset file --- .changeset/sour-gorillas-raise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sour-gorillas-raise.md diff --git a/.changeset/sour-gorillas-raise.md b/.changeset/sour-gorillas-raise.md new file mode 100644 index 0000000000..e1f505c7c6 --- /dev/null +++ b/.changeset/sour-gorillas-raise.md @@ -0,0 +1,5 @@ +--- +'@kadena/client-utils': patch +--- + +Added marmalade functions and correspondent integration testing From 8bb2c25038e93b2b46c0b62b57be3c743e4c271b Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 14 Feb 2024 11:27:26 +0100 Subject: [PATCH 14/28] marmalade integration testing init --- .../integration-tests/marmalade.int.test.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index c267493045..1ccc477a36 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -2,12 +2,15 @@ import type { ChainId } from '@kadena/client'; import { createSignWithKeypair } from '@kadena/client'; import { PactNumber } from '@kadena/pactjs'; import { beforeAll, describe, expect, it } from 'vitest'; +import { describeModule } from '../built-in'; import { transferCreate } from '../coin'; +import { IClientConfig } from '../core/utils/helpers'; import { createToken } from '../marmalade/create-token'; import { createTokenId } from '../marmalade/create-token-id'; import { getBalance } from '../marmalade/get-balance'; import { mintToken } from '../marmalade/mint-token'; import { transferCreateToken } from '../marmalade/transfer-create-token'; +import { deployMarmalade } from '../nodejs'; import { NetworkIds } from './support/NetworkIds'; import { withStepFactory } from './support/helpers'; import { @@ -17,8 +20,9 @@ import { } from './test-data/accounts'; let tokenId: string | undefined; +const chainId = '9' as ChainId; const inputs = { - chainId: '0' as ChainId, + chainId, precision: 0, uri: Date.now().toString(), policies: [], @@ -39,13 +43,38 @@ const config = { }; beforeAll(async () => { - const fundConfig = { + const fundConfig: IClientConfig = { host: 'http://127.0.0.1:8080', defaults: { networkId: 'fast-development', + meta: { + chainId, + }, }, sign: createSignWithKeypair([sender00Account]), }; + let marmaladeDeployed = false; + + try { + await describeModule('marmalade-v2.ledger', fundConfig); + marmaladeDeployed = true; + } catch (error) { + console.log('Marmalade not deployed, deploying now'); + } + + if (!marmaladeDeployed) { + await deployMarmalade({ + chainIds: [chainId], + repositoryConfig: { + owner: 'kadena-io', + name: 'marmalade', + branch: 'main', + githubToken: + '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', + }, + }); + } + const [resultSourceAccount, resultTargetAccount] = await Promise.all([ transferCreate( { @@ -87,7 +116,7 @@ beforeAll(async () => { expect(resultSourceAccount).toBe('Write succeeded'); expect(resultTargetAccount).toBe('Write succeeded'); -}, 30000); +}, 300000); describe('createTokenId', () => { it('should return a token id', async () => { From 9539111e4e3f927e0249b3802b35fa44a292b1d3 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 14 Feb 2024 12:40:39 +0100 Subject: [PATCH 15/28] make chain argument declared at top; --- .../integration-tests/marmalade.int.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 1ccc477a36..9542191876 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -90,7 +90,7 @@ beforeAll(async () => { }, }, amount: '100', - chainId: '0', + chainId, }, fundConfig, ).execute(), @@ -108,7 +108,7 @@ beforeAll(async () => { }, }, amount: '100', - chainId: '0', + chainId, }, fundConfig, ).execute(), @@ -159,7 +159,7 @@ describe('createToken', () => { withStep((step, trDesc) => { expect(step).toBe(3); expect(trDesc.networkId).toBe(NetworkIds.fast_development); - expect(trDesc.chainId).toBe('0'); + expect(trDesc.chainId).toBe(chainId); expect(trDesc.requestKey).toBeTruthy(); }), ) @@ -224,7 +224,7 @@ describe('mintToken', () => { withStep((step, trDesc) => { expect(step).toBe(3); expect(trDesc.networkId).toBe(NetworkIds.fast_development); - expect(trDesc.chainId).toBe('0'); + expect(trDesc.chainId).toBe(chainId); expect(trDesc.requestKey).toBeTruthy(); }), ) @@ -246,7 +246,7 @@ describe('mintToken', () => { const balance = await getBalance( { accountName: sourceAccount.account, - chainId: '0', + chainId, guard: { account: sourceAccount.account, keyset: { @@ -291,7 +291,7 @@ describe('getBalance', () => { const result = await getBalance( { accountName: sourceAccount.account, - chainId: '0', + chainId, guard: { account: sourceAccount.account, keyset: { @@ -311,7 +311,7 @@ describe('getBalance', () => { const task = getBalance( { accountName: sourceAccount.account, - chainId: '0', + chainId, guard: { account: sourceAccount.account, keyset: { @@ -382,7 +382,7 @@ describe('transferCreateToken', () => { withStep((step, trDesc) => { expect(step).toBe(3); expect(trDesc.networkId).toBe(NetworkIds.fast_development); - expect(trDesc.chainId).toBe('0'); + expect(trDesc.chainId).toBe(chainId); expect(trDesc.requestKey).toBeTruthy(); }), ) @@ -404,7 +404,7 @@ describe('transferCreateToken', () => { const balance = await getBalance( { accountName: secondaryTargetAccount.account, - chainId: '0', + chainId: chainId, guard: { account: secondaryTargetAccount.account, keyset: { From b5a5cd50c8c5eaa5fce79db72f3ae214c8e3f056 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 14 Feb 2024 13:18:09 +0100 Subject: [PATCH 16/28] ci changes: add apt-get update --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2b7297d8f..7efe3e2ec1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,8 @@ jobs: - name: Install rust dependencies run: - sudo apt-get install libgtk-3-dev libsoup2.4-dev javascriptcoregtk-4.1 + sudo apt-get update && sudo apt-get install libgtk-3-dev + libsoup2.4-dev javascriptcoregtk-4.1 - name: Install dependencies run: pnpm install From 8953ae356c789a3c30df74eb11b65a63be9dda3c Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 14 Feb 2024 14:04:49 +0100 Subject: [PATCH 17/28] default chain 0 --- .../src/integration-tests/marmalade.int.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 9542191876..c3537ff6f8 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -20,7 +20,7 @@ import { } from './test-data/accounts'; let tokenId: string | undefined; -const chainId = '9' as ChainId; +const chainId = '0' as ChainId; const inputs = { chainId, precision: 0, @@ -65,13 +65,7 @@ beforeAll(async () => { if (!marmaladeDeployed) { await deployMarmalade({ chainIds: [chainId], - repositoryConfig: { - owner: 'kadena-io', - name: 'marmalade', - branch: 'main', - githubToken: - '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', - }, + deleteFilesAfterDeployment: true, }); } From 71c2cf7cf8b45bcf71ef3f14520a7113742b4c93 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Mon, 19 Feb 2024 14:51:31 +0100 Subject: [PATCH 18/28] unit testing to meet threshold --- .../nodejs/marmalade/test/directory.test.ts | 31 ++++- .../src/nodejs/marmalade/test/file.test.ts | 122 +++++++++++++++++- .../src/nodejs/services/download-git-files.ts | 2 +- .../src/nodejs/services/test/path.test.ts | 83 +++++++++++- .../nodejs/test/deploy-from-directory.test.ts | 30 ++++- .../src/nodejs/test/deploy-template.test.ts | 20 +++ packages/libs/client-utils/vitest.config.ts | 6 + 7 files changed, 286 insertions(+), 8 deletions(-) diff --git a/packages/libs/client-utils/src/nodejs/marmalade/test/directory.test.ts b/packages/libs/client-utils/src/nodejs/marmalade/test/directory.test.ts index d12acf2d89..0cd3257756 100644 --- a/packages/libs/client-utils/src/nodejs/marmalade/test/directory.test.ts +++ b/packages/libs/client-utils/src/nodejs/marmalade/test/directory.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync } from 'fs'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { clearDir } from '../../services/path'; -import { handleDirectorySetup } from '../utils/directory'; +import { deleteLocalFiles, handleDirectorySetup } from '../utils/directory'; // Mock the fs and path modules vi.mock('fs', () => ({ @@ -50,3 +50,32 @@ describe('handleDirectorySetup', () => { expect(mkdirSync).not.toHaveBeenCalled(); }); }); +describe('deleteLocalFiles', () => { + it('should call clearDir with the correct paths', () => { + const localConfig = { + templatePath: 'templatePath', + codeFilesPath: 'codeFilesPath', + namespacePath: 'namespacePath', + }; + + deleteLocalFiles(localConfig); + + expect(clearDir).toHaveBeenCalledTimes(3); + expect(clearDir).toHaveBeenCalledWith('templatePath'); + expect(clearDir).toHaveBeenCalledWith('codeFilesPath'); + expect(clearDir).toHaveBeenCalledWith('namespacePath'); + }); + + it('should not call clearDir with duplicate paths', () => { + const localConfig = { + templatePath: 'templatePath', + codeFilesPath: 'templatePath', + namespacePath: 'templatePath', + }; + + deleteLocalFiles(localConfig); + + expect(clearDir).toHaveBeenCalledTimes(1); + expect(clearDir).toHaveBeenCalledWith('templatePath'); + }); +}); diff --git a/packages/libs/client-utils/src/nodejs/marmalade/test/file.test.ts b/packages/libs/client-utils/src/nodejs/marmalade/test/file.test.ts index 950a5b940e..d4b2700965 100644 --- a/packages/libs/client-utils/src/nodejs/marmalade/test/file.test.ts +++ b/packages/libs/client-utils/src/nodejs/marmalade/test/file.test.ts @@ -1,12 +1,22 @@ -import { readdirSync } from 'fs'; +import { readFileSync, readdirSync, writeFileSync } from 'fs'; +import yaml from 'js-yaml'; +import { join, relative } from 'path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { downloadGitFiles } from '../../services/download-git-files'; import { flattenFolder } from '../../services/path'; -import { getMarmaladeTemplates, getNsCodeFiles } from '../utils/file'; +import { IRemoteConfig } from '../deployment/config'; +import { + getCodeFiles, + getMarmaladeTemplates, + getNsCodeFiles, + updateTemplateFilesWithCodeFile, +} from '../utils/file'; // Mock the fs and path modules vi.mock('fs', () => ({ readdirSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), })); vi.mock('../../services/path', () => ({ flattenFolder: vi.fn(), @@ -19,6 +29,7 @@ vi.mock('../../services/download-git-files', () => ({ afterEach(() => { vi.clearAllMocks(); }); + const repositoryConfig = { owner: 'owner', name: 'name', @@ -124,3 +135,110 @@ describe('getNsCodeFiles', () => { }); }); }); +describe('getCodeFiles', () => { + it('should throw an error if no template files are found', async () => { + //@ts-ignore + readdirSync.mockReturnValue([]); + + await expect( + getCodeFiles({ + repositoryConfig, + remoteConfig, + localConfig, + }), + ).rejects.toThrow('No template files found'); + }); + + it('should call downloadGitFiles with the correct arguments', async () => { + //@ts-ignore + readdirSync.mockReturnValue(['file1', 'file2']); + //@ts-ignore + readFileSync.mockReturnValue('codeFile: file'); + + const loadSpy = vi + .spyOn(yaml, 'load') + .mockReturnValue({ codeFile: 'file' }); + + //@ts-ignore + await getCodeFiles({ + repositoryConfig, + remoteConfig, + localConfig, + }); + + expect(downloadGitFiles).toHaveBeenCalledTimes(2); + expect(downloadGitFiles).toHaveBeenCalledWith({ + ...repositoryConfig, + path: 'pact/file', + localPath: localConfig.codeFilesPath, + fileExtension: remoteConfig.codefileExtension, + }); + expect(loadSpy).toHaveBeenCalledTimes(2); + }); +}); +describe('getNsCodeFiles', () => { + it('should call downloadGitFiles with the correct arguments', async () => { + const multipleNsRemoteConfig = { + ...remoteConfig, + namespacePaths: ['path1', 'path2'], + }; + + await getNsCodeFiles({ + repositoryConfig, + remoteConfig: multipleNsRemoteConfig, + localConfig, + }); + + expect(downloadGitFiles).toHaveBeenCalledTimes(2); + expect(downloadGitFiles).toHaveBeenCalledWith({ + ...repositoryConfig, + path: 'path1', + localPath: localConfig.namespacePath, + fileExtension: remoteConfig.codefileExtension, + }); + expect(downloadGitFiles).toHaveBeenCalledWith({ + ...repositoryConfig, + path: 'path2', + localPath: localConfig.namespacePath, + fileExtension: remoteConfig.codefileExtension, + }); + }); +}); + +describe('updateTemplateFilesWithCodeFile', () => { + it('should throw an error if code file is not found', async () => { + //@ts-ignore + readFileSync.mockReturnValue('codeFile: file'); + //@ts-ignore + yaml.load.mockReturnValue({ codeFile: 'file' }); + + await expect( + updateTemplateFilesWithCodeFile( + ['templateFile'], + 'templateDirectory', + ['codeFile'], + 'codeFileDirectory', + ), + ).rejects.toThrow('Code file file not found'); + }); + + it('should call writeFileSync with the correct arguments', async () => { + //@ts-ignore + readFileSync.mockReturnValue('codeFile: file'); + //@ts-ignore + yaml.load.mockReturnValue({ codeFile: 'file' }); + + await updateTemplateFilesWithCodeFile( + ['templateFile'], + 'templateDirectory', + ['file'], + 'codeFileDirectory', + ); + + expect(writeFileSync).toHaveBeenCalledWith( + join('templateDirectory', 'templateFile'), + 'codeFile: ' + + relative('templateDirectory', join('codeFileDirectory', 'file')), + ); + }); +}); diff --git a/packages/libs/client-utils/src/nodejs/services/download-git-files.ts b/packages/libs/client-utils/src/nodejs/services/download-git-files.ts index 454d56a0fa..38f5313c84 100644 --- a/packages/libs/client-utils/src/nodejs/services/download-git-files.ts +++ b/packages/libs/client-utils/src/nodejs/services/download-git-files.ts @@ -17,7 +17,7 @@ export async function downloadGitFiles({ name: string; path: string; branch: string; - localPath: string; + localPath?: string; fileExtension: string; drillDown?: boolean; excludeFolder?: string[]; diff --git a/packages/libs/client-utils/src/nodejs/services/test/path.test.ts b/packages/libs/client-utils/src/nodejs/services/test/path.test.ts index cac9adcd81..d87918152f 100644 --- a/packages/libs/client-utils/src/nodejs/services/test/path.test.ts +++ b/packages/libs/client-utils/src/nodejs/services/test/path.test.ts @@ -43,7 +43,6 @@ describe('flattenFolder', async () => { const statSyncSpy = vi.spyOn(fs, 'statSync'); const renameSyncSpy = vi.spyOn(fs, 'renameSync'); - // Call the function with test values await flattenFolder(basePath, fileExtensions, currentPath); expect(readdirSyncSpy).toHaveBeenNthCalledWith(1, currentPath); @@ -61,6 +60,50 @@ describe('flattenFolder', async () => { `${currentPath}.file2.txt`, ); }); + + it('should make recursive call if a directory is found', async () => { + const basePath = '/test/path'; + const fileExtensions = ['.txt']; + const currentPath = '/test/path/subfolder'; + + readdirSync + //@ts-ignore + .mockImplementationOnce(() => ['directory']) + .mockImplementationOnce(() => ['file1.txt']); + + statSync + //@ts-ignore + .mockImplementationOnce(() => ({ isDirectory: () => true })) + .mockImplementationOnce(() => ({ isDirectory: () => false })); + //@ts-ignore + rmdirSync.mockImplementation(() => {}); + //@ts-ignore + renameSync.mockImplementation(() => {}); + + const readdirSyncSpy = vi.spyOn(fs, 'readdirSync'); + const statSyncSpy = vi.spyOn(fs, 'statSync'); + const renameSyncSpy = vi.spyOn(fs, 'renameSync'); + const rmDirSpry = vi.spyOn(fs, 'rmdirSync'); + + await flattenFolder(basePath, fileExtensions, currentPath); + + expect(readdirSyncSpy).toHaveBeenNthCalledWith(1, currentPath); + expect(readdirSyncSpy).toHaveBeenNthCalledWith( + 2, + `${currentPath}/directory`, + ); + expect(statSyncSpy).toHaveBeenNthCalledWith(1, `${currentPath}/directory`); + expect(statSyncSpy).toHaveBeenNthCalledWith( + 2, + `${currentPath}/directory/file1.txt`, + ); + expect(rmDirSpry).toHaveBeenNthCalledWith(1, `${currentPath}/directory`); + expect(renameSyncSpy).toHaveBeenNthCalledWith( + 1, + `${currentPath}/directory/file1.txt`, + `${currentPath}.directory.file1.txt`, + ); + }); }); describe('clearDir', async () => { @@ -94,7 +137,6 @@ describe('clearDir', async () => { const dir = '/test/path'; const extension = '.txt'; - //@ts-ignore //@ts-ignore readdirSync.mockImplementation(() => ['file1.txt', 'file2.txt']); //@ts-ignore @@ -104,7 +146,43 @@ describe('clearDir', async () => { throw new Error('error'); }); + vi.spyOn(console, 'error'); + clearDir(dir, extension); + + expect(console.error).toHaveBeenCalledWith( + `Error processing file ${dir}/file1.txt: Error: error`, + ); + expect(console.error).toHaveBeenCalledWith( + `Error processing file ${dir}/file2.txt: Error: error`, + ); + }); + + it('should make recursive call if filepath is directory', async () => { + const dir = '/test/path'; + const extension = '.txt'; + + readdirSync + //@ts-ignore + .mockImplementationOnce(() => ['file1.txt', 'directory1']) + .mockImplementationOnce(() => ['file2.txt']); + + statSync + //@ts-ignore + .mockImplementationOnce(() => ({ isDirectory: () => false })) + .mockImplementationOnce(() => ({ isDirectory: () => true })); + //@ts-ignore + unlinkSync.mockImplementation(() => {}); + + clearDir(dir, extension); + + expect(readdirSync).toHaveBeenNthCalledWith(1, dir); + expect(readdirSync).toHaveBeenNthCalledWith(2, `${dir}/directory1`); + expect(unlinkSync).toHaveBeenNthCalledWith(1, `${dir}/file1.txt`); + expect(unlinkSync).toHaveBeenNthCalledWith( + 2, + `${dir}/directory1/file2.txt`, + ); }); }); @@ -122,7 +200,6 @@ describe('createDirAndWriteFile', async () => { const mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync'); const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync'); - // Call the function with test values await createDirAndWriteFile(dir, fileName, data); expect(mkdirSyncSpy).toHaveBeenNthCalledWith(1, dir, { recursive: true }); diff --git a/packages/libs/client-utils/src/nodejs/test/deploy-from-directory.test.ts b/packages/libs/client-utils/src/nodejs/test/deploy-from-directory.test.ts index a0c4ea4fdb..cd0fada030 100644 --- a/packages/libs/client-utils/src/nodejs/test/deploy-from-directory.test.ts +++ b/packages/libs/client-utils/src/nodejs/test/deploy-from-directory.test.ts @@ -24,7 +24,7 @@ describe('deploy-from-directory', () => { ]), }; - describe('deploying templates', () => { + describe('deploying templates', async () => { const templateConfig = { path: 'src/nodejs/test/aux-files/deploy-from-directory/template/', extension: 'yaml', @@ -154,6 +154,34 @@ describe('deploy-from-directory', () => { expect(deployTemplateSpy).toHaveBeenCalledTimes(2); }); + + it('should throw an error if no template or code files are provided', async () => { + await expect( + deployFromDirectory({ + chainIds: [chainId], + clientConfig, + }), + ).rejects.toThrow('Please provide a template or a code files directory'); + }); + + it('should catch and log the error if one occurs', async () => { + const deployTemplateSpy = vi + .spyOn(deployTemplate, 'deployTemplate') + .mockImplementation(() => { + throw new Error('test error'); + }); + + const consoleSpy = vi.spyOn(console, 'error'); + + await deployFromDirectory({ + chainIds: [chainId], + templateConfig, + clientConfig, + }); + + expect(consoleSpy).toHaveBeenCalledTimes(2); + expect(deployTemplateSpy).toHaveBeenCalledTimes(2); + }); }); describe('deploying code files', () => { diff --git a/packages/libs/client-utils/src/nodejs/test/deploy-template.test.ts b/packages/libs/client-utils/src/nodejs/test/deploy-template.test.ts index ecc6c48931..7638fc571b 100644 --- a/packages/libs/client-utils/src/nodejs/test/deploy-template.test.ts +++ b/packages/libs/client-utils/src/nodejs/test/deploy-template.test.ts @@ -84,4 +84,24 @@ describe('deploy-template', () => { __dirname, ); }); + it('should call createPactCommandFromTemplate with an empty object', async () => { + const createPactCommandFromTemplateSpy = vi + .spyOn(yamlConverter, 'createPactCommandFromTemplate') + .mockResolvedValueOnce({} as IPactCommand); + + await deployTemplate( + { + templatePath: testFile, + workingDirectory: __dirname, + }, + clientConfig, + ); + + expect(createPactCommandFromTemplateSpy).toHaveBeenCalledTimes(1); + expect(createPactCommandFromTemplateSpy).toHaveBeenCalledWith( + testFile, + {}, + __dirname, + ); + }); }); diff --git a/packages/libs/client-utils/vitest.config.ts b/packages/libs/client-utils/vitest.config.ts index 13463af921..f48e281ec5 100644 --- a/packages/libs/client-utils/vitest.config.ts +++ b/packages/libs/client-utils/vitest.config.ts @@ -12,12 +12,18 @@ const localConfig = defineConfig({ 'src/built-in/**/*', // we have integration for this 'src/core/estimate-gas.ts', + // we have integration for this + 'src/marmalade/**/*', + // this service interacts with github + 'src/nodejs/services/download-git-files.ts', // its just type 'src/interfaces/async-pipe-type.ts', // its just type and I have a test for that 'src/core/utils/types.ts', // its a script that generates the asyncPipe type 'src/scripts/**/*', + // its a script and auxiliary files that deploys marmalade namespaces and contracts + 'src/nodejs/marmalade/deployment/**/*', ], provider: 'v8', thresholds: { From a94a97ddae978b0e99371f16de46232ed0da4ada Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Mon, 19 Feb 2024 16:02:21 +0100 Subject: [PATCH 19/28] lint fixes --- .../client-utils/src/integration-tests/marmalade.int.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index c3537ff6f8..a325901fb9 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -4,7 +4,7 @@ import { PactNumber } from '@kadena/pactjs'; import { beforeAll, describe, expect, it } from 'vitest'; import { describeModule } from '../built-in'; import { transferCreate } from '../coin'; -import { IClientConfig } from '../core/utils/helpers'; +import type { IClientConfig } from '../core/utils/helpers'; import { createToken } from '../marmalade/create-token'; import { createTokenId } from '../marmalade/create-token-id'; import { getBalance } from '../marmalade/get-balance'; From 72463b0bbc501e8d4b79ac4246dad5e8c42845a9 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Tue, 12 Mar 2024 13:59:55 +0100 Subject: [PATCH 20/28] fix: minor fixes and comments resolutions --- packages/libs/client-utils/package.json | 4 +--- .../integration-tests/marmalade.int.test.ts | 22 +++++++++---------- .../{get-balance.ts => get-token-balance.ts} | 9 +++++--- .../libs/client-utils/src/marmalade/index.ts | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) rename packages/libs/client-utils/src/marmalade/{get-balance.ts => get-token-balance.ts} (87%) diff --git a/packages/libs/client-utils/package.json b/packages/libs/client-utils/package.json index 53419fb645..3e35fc668c 100644 --- a/packages/libs/client-utils/package.json +++ b/packages/libs/client-utils/package.json @@ -113,9 +113,7 @@ "lint:fmt": "prettier . --cache --check", "lint:pkg": "lint-package", "lint:src": "eslint src --ext .js,.ts", - "pactjs:generate:contract": "pnpm run pactjs:generate:contract:coin && pnpm run pactjs:generate:contract:marmalade", - "pactjs:generate:contract:coin": "pactjs contract-generate --contract coin --api https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/0/pact", - "pactjs:generate:contract:marmalade": "pactjs contract-generate --contract marmalade-v2.ledger --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact;", + "pactjs:generate:contract": "pactjs contract-generate --contract coin --contract marmalade-v2.ledger --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact", "test": "vitest run", "test:integration": "vitest run -c ./vitest.integration.config.ts", "test:watch": "vitest" diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index a325901fb9..21b6e83139 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -7,7 +7,7 @@ import { transferCreate } from '../coin'; import type { IClientConfig } from '../core/utils/helpers'; import { createToken } from '../marmalade/create-token'; import { createTokenId } from '../marmalade/create-token-id'; -import { getBalance } from '../marmalade/get-balance'; +import { getTokenBalance } from '../marmalade/get-token-balance'; import { mintToken } from '../marmalade/mint-token'; import { transferCreateToken } from '../marmalade/transfer-create-token'; import { deployMarmalade } from '../nodejs'; @@ -37,7 +37,7 @@ const inputs = { const config = { host: 'http://127.0.0.1:8080', defaults: { - networkId: 'fast-development', + networkId: 'development', }, sign: createSignWithKeypair([sourceAccount]), }; @@ -46,7 +46,7 @@ beforeAll(async () => { const fundConfig: IClientConfig = { host: 'http://127.0.0.1:8080', defaults: { - networkId: 'fast-development', + networkId: 'development', meta: { chainId, }, @@ -152,7 +152,7 @@ describe('createToken', () => { 'submit', withStep((step, trDesc) => { expect(step).toBe(3); - expect(trDesc.networkId).toBe(NetworkIds.fast_development); + expect(trDesc.networkId).toBe(NetworkIds.development); expect(trDesc.chainId).toBe(chainId); expect(trDesc.requestKey).toBeTruthy(); }), @@ -217,7 +217,7 @@ describe('mintToken', () => { 'submit', withStep((step, trDesc) => { expect(step).toBe(3); - expect(trDesc.networkId).toBe(NetworkIds.fast_development); + expect(trDesc.networkId).toBe(NetworkIds.development); expect(trDesc.chainId).toBe(chainId); expect(trDesc.requestKey).toBeTruthy(); }), @@ -237,7 +237,7 @@ describe('mintToken', () => { expect(result).toBe(true); - const balance = await getBalance( + const balance = await getTokenBalance( { accountName: sourceAccount.account, chainId, @@ -280,9 +280,9 @@ describe('mintToken', () => { }); }); -describe('getBalance', () => { +describe('getTokenBalance', () => { it('shoud get a balance', async () => { - const result = await getBalance( + const result = await getTokenBalance( { accountName: sourceAccount.account, chainId, @@ -302,7 +302,7 @@ describe('getBalance', () => { }); it('should throw an error if token does not exist', async () => { const nonExistingTokenId = 'non-existing-token'; - const task = getBalance( + const task = getTokenBalance( { accountName: sourceAccount.account, chainId, @@ -375,7 +375,7 @@ describe('transferCreateToken', () => { 'submit', withStep((step, trDesc) => { expect(step).toBe(3); - expect(trDesc.networkId).toBe(NetworkIds.fast_development); + expect(trDesc.networkId).toBe(NetworkIds.development); expect(trDesc.chainId).toBe(chainId); expect(trDesc.requestKey).toBeTruthy(); }), @@ -395,7 +395,7 @@ describe('transferCreateToken', () => { expect(result).toBe(true); - const balance = await getBalance( + const balance = await getTokenBalance( { accountName: secondaryTargetAccount.account, chainId: chainId, diff --git a/packages/libs/client-utils/src/marmalade/get-balance.ts b/packages/libs/client-utils/src/marmalade/get-token-balance.ts similarity index 87% rename from packages/libs/client-utils/src/marmalade/get-balance.ts rename to packages/libs/client-utils/src/marmalade/get-token-balance.ts index 80a52d7576..5ab5ff3bdd 100644 --- a/packages/libs/client-utils/src/marmalade/get-balance.ts +++ b/packages/libs/client-utils/src/marmalade/get-token-balance.ts @@ -24,7 +24,7 @@ interface IGeBalanceInput { }; } -const getBalanceCommand = ({ +const getTokenBalanceCommand = ({ tokenId, chainId, accountName, @@ -45,7 +45,10 @@ const getBalanceCommand = ({ }), ); -export const getBalance = (inputs: IGeBalanceInput, config: IClientConfig) => +export const getTokenBalance = ( + inputs: IGeBalanceInput, + config: IClientConfig, +) => submitClient< PactReturnType - >(config)(getBalanceCommand(inputs)); + >(config)(getTokenBalanceCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index 25db8e5162..bcb8796d10 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -1,5 +1,5 @@ export * from './create-token'; export * from './create-token-id'; -export * from './get-balance'; +export * from './get-token-balance'; export * from './mint-token'; export * from './transfer-create-token'; From 8fa5352912bae00d23a69ddfdb7e77ffcd2000f7 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 3 Apr 2024 11:50:15 +0100 Subject: [PATCH 21/28] fix: adjusted precision type from number to Pact Int --- .../src/integration-tests/marmalade.int.test.ts | 2 +- .../libs/client-utils/src/marmalade/create-token-id.ts | 10 ++++++++-- .../libs/client-utils/src/marmalade/create-token.ts | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 21b6e83139..5e5b7bb3a4 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -23,7 +23,7 @@ let tokenId: string | undefined; const chainId = '0' as ChainId; const inputs = { chainId, - precision: 0, + precision: { int: '0' }, uri: Date.now().toString(), policies: [], creator: { diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index bf3ffc6952..4cc0765595 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -1,4 +1,9 @@ -import type { ChainId, IPactModules, PactReturnType } from '@kadena/client'; +import type { + ChainId, + IPactModules, + PactReference, + PactReturnType, +} from '@kadena/client'; import { Pact, readKeyset } from '@kadena/client'; import { addKeyset, @@ -6,13 +11,14 @@ import { execution, setMeta, } from '@kadena/client/fp'; +import { IPactInt } from '@kadena/types'; import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; interface ICreateTokenIdInput { policies?: string[]; uri: string; - precision: number; + precision: IPactInt | PactReference; chainId: ChainId; creator: { account: string; diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 3c15877bb1..2f68c811d3 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -12,7 +12,7 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import { PactNumber } from '@kadena/pactjs'; +import { IPactInt, PactValue } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; @@ -20,7 +20,7 @@ interface ICreateTokenInput { policies?: string[]; uri: string; tokenId: string; - precision: number; + precision: IPactInt | PactReference; chainId: ChainId; creator: { account: string; @@ -43,7 +43,7 @@ const createTokenCommand = ({ execution( Pact.modules['marmalade-v2.ledger']['create-token']( tokenId, - new PactNumber(precision).toPactInteger(), + precision, uri, policies.length > 0 ? ([policies.join(' ')] as unknown as PactReference) From cdd1af6f6eea49e1d82a72e6f5f9156d82df7250 Mon Sep 17 00:00:00 2001 From: Nil Amrutlal Date: Wed, 3 Apr 2024 11:58:24 +0100 Subject: [PATCH 22/28] fix: lint fixes --- packages/libs/client-utils/src/marmalade/create-token-id.ts | 2 +- packages/libs/client-utils/src/marmalade/create-token.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index 4cc0765595..efad933a74 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -11,7 +11,7 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import { IPactInt } from '@kadena/types'; +import type { IPactInt } from '@kadena/types'; import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 2f68c811d3..a043c470c8 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -12,7 +12,7 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import { IPactInt, PactValue } from '@kadena/types'; +import type { IPactInt } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; From 5ad87a5a95fb29013c8293826a1922294b525630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Maga=C5=A1?= Date: Fri, 3 May 2024 12:43:17 +0200 Subject: [PATCH 23/28] chore: update the create token capability signature --- packages/libs/client-utils/src/marmalade/create-token.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index a043c470c8..a79aa81644 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -54,10 +54,7 @@ const createTokenCommand = ({ addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), addSigner(creator.keyset.keys, (signFor) => [ signFor('coin.GAS'), - signFor('marmalade-v2.ledger.CREATE-TOKEN', tokenId, { - pred: creator.keyset.pred, - keys: creator.keyset.keys, - }), + signFor('marmalade-v2.ledger.CREATE-TOKEN', tokenId), ]), setMeta({ senderAccount: creator.account, chainId }), ); From a0153c45ddd3251b68a52282a4271521c206751f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Maga=C5=A1?= Date: Fri, 3 May 2024 16:07:09 +0200 Subject: [PATCH 24/28] [@kadena/client-utils] Implement Marmalade functions - additions (#1945) * chore: added more maramalade util functions * chore: align create-token with other functions * fix: linting issues --- .../integration-tests/marmalade.int.test.ts | 286 +++++++++++++++++- .../client-utils/src/marmalade/burn-token.ts | 77 +++++ .../src/marmalade/create-token-id.ts | 14 +- .../src/marmalade/create-token.ts | 106 ++++++- .../src/marmalade/get-account-details.ts | 53 ++++ .../src/marmalade/get-token-balance.ts | 19 +- .../src/marmalade/get-token-info.ts | 45 +++ .../client-utils/src/marmalade/get-uri.ts | 42 +++ .../libs/client-utils/src/marmalade/index.ts | 6 + .../client-utils/src/marmalade/mint-token.ts | 34 ++- .../src/marmalade/policy-config.ts | 119 ++++++++ .../src/marmalade/transfer-create-token.ts | 25 +- .../src/marmalade/transfer-token.ts | 85 ++++++ 13 files changed, 878 insertions(+), 33 deletions(-) create mode 100644 packages/libs/client-utils/src/marmalade/burn-token.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-account-details.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-token-info.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-uri.ts create mode 100644 packages/libs/client-utils/src/marmalade/policy-config.ts create mode 100644 packages/libs/client-utils/src/marmalade/transfer-token.ts diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 5e5b7bb3a4..580bceb785 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -5,6 +5,12 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { describeModule } from '../built-in'; import { transferCreate } from '../coin'; import type { IClientConfig } from '../core/utils/helpers'; +import { + burnToken, + getAccountDetails, + getTokenInfo, + getUri, +} from '../marmalade'; import { createToken } from '../marmalade/create-token'; import { createTokenId } from '../marmalade/create-token-id'; import { getTokenBalance } from '../marmalade/get-token-balance'; @@ -122,7 +128,7 @@ describe('createTokenId', () => { }); describe('createToken', () => { - it('shoud create a token', async () => { + it('should create a token', async () => { const withStep = withStepFactory(); const result = await createToken( @@ -175,14 +181,14 @@ describe('createToken', () => { }); describe('mintToken', () => { - it('shoud mint a token', async () => { + it('should mint a token', async () => { const withStep = withStepFactory(); const result = await mintToken( { ...inputs, tokenId: tokenId as string, - creatorAccount: sourceAccount.account, + accountName: sourceAccount.account, guard: { account: sourceAccount.account, keyset: { @@ -255,13 +261,13 @@ describe('mintToken', () => { expect(balance).toBe(1); }); - it('shoud throw error when non-existent token is minted', async () => { + it('should throw error when non-existent token is minted', async () => { const nonExistingTokenId = 'non-existing-token'; const task = mintToken( { ...inputs, tokenId: nonExistingTokenId, - creatorAccount: sourceAccount.account, + accountName: sourceAccount.account, guard: { account: sourceAccount.account, keyset: { @@ -281,7 +287,7 @@ describe('mintToken', () => { }); describe('getTokenBalance', () => { - it('shoud get a balance', async () => { + it('should get a balance', async () => { const result = await getTokenBalance( { accountName: sourceAccount.account, @@ -326,8 +332,152 @@ describe('getTokenBalance', () => { }); }); +describe('getTokenInfo', () => { + it('should get the info', async () => { + const result = await getTokenInfo( + { + chainId, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + config, + ).execute(); + + expect(result).toStrictEqual({ + supply: 1, + precision: { int: 0 }, + uri: inputs.uri, + id: tokenId, + policies: [], + }); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = getTokenInfo( + { + chainId, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: nonExistingTokenId, + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error(`with-read: row not found: ${nonExistingTokenId}`), + ); + }); +}); + +describe('getTokenUri', () => { + it('should get the uri', async () => { + const result = await getUri( + { + chainId, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + config, + ).execute(); + + expect(result).toBe(inputs.uri); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = getUri( + { + chainId, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: nonExistingTokenId, + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error(`with-read: row not found: ${nonExistingTokenId}`), + ); + }); +}); + +describe('getAccountDetails', () => { + it('should get the account details', async () => { + const result = await getAccountDetails( + { + chainId, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + config, + ).execute(); + + expect(result).toStrictEqual({ + account: sourceAccount.account, + balance: 1, + guard: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + id: tokenId, + }); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = getAccountDetails( + { + chainId, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: nonExistingTokenId, + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error( + `read: row not found: ${nonExistingTokenId}:${sourceAccount.account}`, + ), + ); + }); +}); + describe('transferCreateToken', () => { - it('shoud transfer-create a token', async () => { + it('should transfer-create a token', async () => { const withStep = withStepFactory(); const result = await transferCreateToken( @@ -442,3 +592,125 @@ describe('transferCreateToken', () => { ); }); }); + +describe('burnToken', () => { + it('should burn a token', async () => { + const withStep = withStepFactory(); + + const burnConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const result = await burnToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: secondaryTargetAccount.account, + guard: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + burnConfig, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getTokenBalance( + { + accountName: secondaryTargetAccount.account, + chainId, + guard: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + tokenId: tokenId as string, + }, + burnConfig, + ).execute(); + + expect(balance).toBe(0); + }); + it('should throw error when non-existent token is burned', async () => { + const burnConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const nonExistingTokenId = 'non-existing-token'; + const task = burnToken( + { + ...inputs, + tokenId: nonExistingTokenId, + accountName: secondaryTargetAccount.account, + guard: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + burnConfig, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('with-read: row not found: non-existing-token'), + ); + }); +}); diff --git a/packages/libs/client-utils/src/marmalade/burn-token.ts b/packages/libs/client-utils/src/marmalade/burn-token.ts new file mode 100644 index 0000000000..41963d6708 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/burn-token.ts @@ -0,0 +1,77 @@ +import type { + BuiltInPredicate, + IPactModules, + PactReturnType, +} from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId, IPactDecimal } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IBurnTokenInput { + policyConfig?: { + guarded?: boolean; + nonFungible?: boolean; + hasRoyalty?: boolean; + }; + tokenId: string; + accountName: string; + chainId: ChainId; + guard: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; + amount: IPactDecimal; +} + +const burnTokenCommand = ({ + tokenId, + accountName, + chainId, + guard, + amount, + policyConfig, +}: IBurnTokenInput) => { + if (policyConfig?.nonFungible) { + throw new Error('Non-fungible tokens cannot be burned'); + } + + if (policyConfig?.hasRoyalty) { + throw new Error('Royalty tokens cannot be burned'); + } + + return composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger'].burn(tokenId, accountName, amount), + ), + addSigner(guard.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor('marmalade-v2.ledger.BURN', tokenId, accountName, amount), + ...(!!policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.BURN', + tokenId, + accountName, + amount, + ), + ] + : []), + ]), + setMeta({ senderAccount: guard.account, chainId }), + ); +}; + +export const burnToken = (inputs: IBurnTokenInput, config: IClientConfig) => + submitClient>( + config, + )(burnTokenCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index efad933a74..4e2c451412 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -1,4 +1,5 @@ import type { + BuiltInPredicate, ChainId, IPactModules, PactReference, @@ -14,8 +15,11 @@ import { import type { IPactInt } from '@kadena/types'; import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; +import type { ICreateTokenPolicyConfig } from './policy-config'; +import { validatePolicies } from './policy-config'; interface ICreateTokenIdInput { + policyConfig?: ICreateTokenPolicyConfig; policies?: string[]; uri: string; precision: IPactInt | PactReference; @@ -24,7 +28,7 @@ interface ICreateTokenIdInput { account: string; keyset: { keys: string[]; - pred: 'keys-all' | 'keys-2' | 'keys-any'; + pred: BuiltInPredicate; }; }; } @@ -35,8 +39,11 @@ const createTokenIdCommand = ({ precision, creator, chainId, -}: ICreateTokenIdInput) => - composePactCommand( + policyConfig, +}: ICreateTokenIdInput) => { + validatePolicies(policyConfig, policies); + + return composePactCommand( execution( Pact.modules['marmalade-v2.ledger']['create-token-id']( { precision, uri, policies }, @@ -46,6 +53,7 @@ const createTokenIdCommand = ({ addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), setMeta({ senderAccount: creator.account, chainId }), ); +}; export const createTokenId = ( inputs: ICreateTokenIdInput, diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index a79aa81644..56352e80cb 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -1,20 +1,31 @@ import type { + BuiltInPredicate, ChainId, + ICap, IPactModules, + IPartialPactCommand, PactReference, PactReturnType, } from '@kadena/client'; import { Pact, readKeyset } from '@kadena/client'; import { + addData, addKeyset, addSigner, composePactCommand, execution, setMeta, } from '@kadena/client/fp'; +import type { IGeneralCapability } from '@kadena/client/lib/interfaces/type-utilities'; import type { IPactInt } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; +import type { + ICreateTokenPolicyConfig, + PolicyProps, + WithCreateTokenPolicy, +} from './policy-config'; +import { validatePolicies } from './policy-config'; interface ICreateTokenInput { policies?: string[]; @@ -26,20 +37,87 @@ interface ICreateTokenInput { account: string; keyset: { keys: string[]; - pred: 'keys-all' | 'keys-2' | 'keys-any'; + pred: BuiltInPredicate; }; }; } -const createTokenCommand = ({ +const generatePolicyCapabilities = ( + policyConfig: ICreateTokenPolicyConfig, + props: PolicyProps & { tokenId: string }, + signFor: IGeneralCapability, +): ICap[] => { + const capabilities = []; + + if (policyConfig?.collection) { + capabilities.push( + signFor( + 'marmalade-v2.collection-policy-v1.TOKEN-COLLECTION', + props.collection.collectionId, + props.tokenId, + ), + ); + } + + return capabilities; +}; + +const generatePolicyTransactionData = ( + policyConfig: ICreateTokenPolicyConfig, + props: PolicyProps, +): ((cmd: IPartialPactCommand) => IPartialPactCommand)[] => { + const data = []; + + if (policyConfig?.collection) { + data.push(addData('collection_id', props.collection.collectionId)); + } + + if (policyConfig?.guarded) { + if (props.guards.mintGuard) + data.push(addData('mint_guard', props.guards.mintGuard)); + if (props.guards.burnGuard) + data.push(addData('burn_guard', props.guards.burnGuard)); + if (props.guards.saleGuard) + data.push(addData('sale_guard', props.guards.saleGuard)); + if (props.guards.transferGuard) + data.push(addData('transfer_guard', props.guards.transferGuard)); + if (props.guards.uriGuard) + data.push(addData('uri_guard', props.guards.uriGuard)); + } + + if (policyConfig?.hasRoyalty) { + data.push(addData('fungible', props.royalty.fungible)); + data.push(addData('creator', props.royalty.creator)); + data.push(addData('creator-guard', props.royalty.creatorGuard)); + data.push(addData('royalty-rate', props.royalty.royaltyRate.decimal)); + } + + if (policyConfig?.upgradeableURI && !policyConfig?.guarded) { + if (props.guards.uriGuard) + data.push(addData('uri_guard', props.guards.uriGuard)); + } + + if (policyConfig?.customPolicies) { + for (const key of Object.keys(props.customPolicyData)) { + data.push(addData(key, props.customPolicyData[key])); + } + } + + return data; +}; + +const createTokenCommand = ({ policies = [], uri, tokenId, precision, creator, chainId, -}: ICreateTokenInput) => - composePactCommand( + policyConfig, + ...policyProps +}: WithCreateTokenPolicy) => { + validatePolicies(policyConfig as ICreateTokenPolicyConfig, policies); + return composePactCommand( execution( Pact.modules['marmalade-v2.ledger']['create-token']( tokenId, @@ -51,15 +129,31 @@ const createTokenCommand = ({ readKeyset('creation-guard'), ), ), + setMeta({ senderAccount: creator.account, chainId }), addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), addSigner(creator.keyset.keys, (signFor) => [ signFor('coin.GAS'), signFor('marmalade-v2.ledger.CREATE-TOKEN', tokenId), + + ...generatePolicyCapabilities( + policyConfig as ICreateTokenPolicyConfig, + { ...policyProps, tokenId } as unknown as PolicyProps & { + tokenId: string; + }, + signFor, + ), ]), - setMeta({ senderAccount: creator.account, chainId }), + ...generatePolicyTransactionData( + policyConfig as ICreateTokenPolicyConfig, + policyProps as unknown as PolicyProps, + ), ); +}; -export const createToken = (inputs: ICreateTokenInput, config: IClientConfig) => +export const createToken = ( + inputs: WithCreateTokenPolicy, + config: IClientConfig, +) => submitClient< PactReturnType >(config)(createTokenCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/get-account-details.ts b/packages/libs/client-utils/src/marmalade/get-account-details.ts new file mode 100644 index 0000000000..cc757a3e95 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-account-details.ts @@ -0,0 +1,53 @@ +import type { + BuiltInPredicate, + IPactModules, + PactReturnType, +} from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetAccountBalanceInput { + tokenId: string; + accountName: string; + chainId: ChainId; + guard: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; +} + +const getAccountDetailsCommand = ({ + tokenId, + accountName, + chainId, + guard, +}: IGetAccountBalanceInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['details'](tokenId, accountName), + ), + addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), + setMeta({ + senderAccount: guard.account, + chainId, + }), + ); + +export const getAccountDetails = ( + inputs: IGetAccountBalanceInput, + config: IClientConfig, +) => + submitClient>( + config, + )(getAccountDetailsCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/get-token-balance.ts b/packages/libs/client-utils/src/marmalade/get-token-balance.ts index 5ab5ff3bdd..9eeb4dca9e 100644 --- a/packages/libs/client-utils/src/marmalade/get-token-balance.ts +++ b/packages/libs/client-utils/src/marmalade/get-token-balance.ts @@ -1,4 +1,8 @@ -import type { IPactModules, PactReturnType } from '@kadena/client'; +import type { + BuiltInPredicate, + IPactModules, + PactReturnType, +} from '@kadena/client'; import { Pact } from '@kadena/client'; import { addKeyset, @@ -11,7 +15,7 @@ import type { ChainId } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; -interface IGeBalanceInput { +interface IGetBalanceInput { tokenId: string; chainId: ChainId; accountName: string; @@ -19,7 +23,7 @@ interface IGeBalanceInput { account: string; keyset: { keys: string[]; - pred: 'keys-all' | 'keys-2' | 'keys-any'; + pred: BuiltInPredicate; }; }; } @@ -29,16 +33,13 @@ const getTokenBalanceCommand = ({ chainId, accountName, guard, -}: IGeBalanceInput) => +}: IGetBalanceInput) => composePactCommand( execution( Pact.modules['marmalade-v2.ledger']['get-balance'](tokenId, accountName), ), addKeyset('guard', 'keys-all', ...guard.keyset.keys), - addSigner(guard.keyset.keys, (signFor) => [ - signFor('coin.GAS'), - signFor('marmalade-v2.ledger.get-balance', tokenId, accountName), - ]), + addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), setMeta({ senderAccount: guard.account, chainId, @@ -46,7 +47,7 @@ const getTokenBalanceCommand = ({ ); export const getTokenBalance = ( - inputs: IGeBalanceInput, + inputs: IGetBalanceInput, config: IClientConfig, ) => submitClient< diff --git a/packages/libs/client-utils/src/marmalade/get-token-info.ts b/packages/libs/client-utils/src/marmalade/get-token-info.ts new file mode 100644 index 0000000000..73f80feabf --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-token-info.ts @@ -0,0 +1,45 @@ +import type { + BuiltInPredicate, + IPactModules, + PactReturnType, +} from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetTokenInfoInput { + tokenId: string; + chainId: ChainId; + guard: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; +} + +const getTokenInfoCommand = ({ tokenId, chainId, guard }: IGetTokenInfoInput) => + composePactCommand( + execution(Pact.modules['marmalade-v2.ledger']['get-token-info'](tokenId)), + addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), + setMeta({ + senderAccount: guard.account, + chainId, + }), + ); + +export const getTokenInfo = ( + inputs: IGetTokenInfoInput, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(getTokenInfoCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/get-uri.ts b/packages/libs/client-utils/src/marmalade/get-uri.ts new file mode 100644 index 0000000000..e89a10ccc6 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-uri.ts @@ -0,0 +1,42 @@ +import type { + BuiltInPredicate, + IPactModules, + PactReturnType, +} from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetUriInput { + tokenId: string; + chainId: ChainId; + guard: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; +} + +const getUriCommand = ({ tokenId, chainId, guard }: IGetUriInput) => + composePactCommand( + execution(Pact.modules['marmalade-v2.ledger']['get-uri'](tokenId)), + addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), + setMeta({ + senderAccount: guard.account, + chainId, + }), + ); + +export const getUri = (inputs: IGetUriInput, config: IClientConfig) => + submitClient>( + config, + )(getUriCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index bcb8796d10..40c01e8d9f 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -1,5 +1,11 @@ +export * from './burn-token'; export * from './create-token'; export * from './create-token-id'; +export * from './get-account-details'; export * from './get-token-balance'; +export * from './get-token-info'; +export * from './get-uri'; export * from './mint-token'; +export * from './policy-config'; export * from './transfer-create-token'; +export * from './transfer-token'; diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts index 0ebf4e7043..8201f2a811 100644 --- a/packages/libs/client-utils/src/marmalade/mint-token.ts +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -12,8 +12,12 @@ import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; interface IMintTokenInput { + policyConfig?: { + guarded?: boolean; + nonFungible?: boolean; + }; tokenId: string; - creatorAccount: string; + accountName: string; chainId: ChainId; guard: { account: string; @@ -27,16 +31,23 @@ interface IMintTokenInput { const mintTokenCommand = ({ tokenId, - creatorAccount, + accountName, chainId, guard, amount, -}: IMintTokenInput) => - composePactCommand( + policyConfig, +}: IMintTokenInput) => { + if (policyConfig?.nonFungible && amount.decimal !== '1') { + throw new Error( + 'Non-fungible tokens can only be minted with an amount of 1', + ); + } + + return composePactCommand( execution( Pact.modules['marmalade-v2.ledger'].mint( tokenId, - creatorAccount, + accountName, readKeyset('guard'), amount, ), @@ -44,10 +55,21 @@ const mintTokenCommand = ({ addKeyset('guard', guard.keyset.pred, ...guard.keyset.keys), addSigner(guard.keyset.keys, (signFor) => [ signFor('coin.GAS'), - signFor('marmalade-v2.ledger.MINT', tokenId, creatorAccount, amount), + signFor('marmalade-v2.ledger.MINT', tokenId, accountName, amount), + ...(!!policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.MINT', + tokenId, + accountName, + amount, + ), + ] + : []), ]), setMeta({ senderAccount: guard.account, chainId }), ); +}; export const mintToken = (inputs: IMintTokenInput, config: IClientConfig) => submitClient>( diff --git a/packages/libs/client-utils/src/marmalade/policy-config.ts b/packages/libs/client-utils/src/marmalade/policy-config.ts new file mode 100644 index 0000000000..b5404908c4 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/policy-config.ts @@ -0,0 +1,119 @@ +import type { BuiltInPredicate } from '@kadena/client'; +import type { IPactDecimal } from '@kadena/types'; +export interface IRoyaltyInfoInput { + fungible: string; + creator: string; + creatorGuard: { + keys: string[]; + pred: BuiltInPredicate; + }; + royaltyRate: IPactDecimal; +} + +export interface IGuardInfoInput { + mintGuard?: { + keys: string[]; + pred: BuiltInPredicate; + }; + uriGuard?: { + keys: string[]; + pred: BuiltInPredicate; + }; + saleGuard?: { + keys: string[]; + pred: BuiltInPredicate; + }; + burnGuard?: { + keys: string[]; + pred: BuiltInPredicate; + }; + transferGuard?: { + keys: string[]; + pred: BuiltInPredicate; + }; +} + +export interface ICollectionInfoInput { + collectionId: string; +} + +export interface ICreateTokenPolicyConfig { + customPolicies?: boolean; + upgradeableURI?: boolean; + guarded?: boolean; + nonFungible?: boolean; + hasRoyalty?: boolean; + collection?: boolean; +} + +export const GUARD_POLICY = 'guard-policy-v1'; +export const NON_FUNGIBLE_POLICY = 'non-fungible-policy-v1'; +export const ROYALTY_POLICY = 'royalty-policy-v1'; +export const COLLECTION_POLICY = 'collection-policy-v1'; + +type ConfigToDataMap = { + customPolicies: { customPolicyData: Record }; + upgradeableURI: {}; + guarded: { guards: IGuardInfoInput }; + nonFungible: {}; + hasRoyalty: { royalty: IRoyaltyInfoInput }; + collection: { collection: ICollectionInfoInput }; +}; + +export type PolicyProps = { + customPolicyData: Record; + guards: IGuardInfoInput; + royalty: IRoyaltyInfoInput; + collection: ICollectionInfoInput; +}; + +type PolicyDataForConfig = + (C['customPolicies'] extends true ? ConfigToDataMap['customPolicies'] : {}) & + (C['upgradeableURI'] extends true + ? ConfigToDataMap['upgradeableURI'] + : {}) & + (C['guarded'] extends true ? ConfigToDataMap['guarded'] : {}) & + (C['nonFungible'] extends true ? ConfigToDataMap['nonFungible'] : {}) & + (C['hasRoyalty'] extends true ? ConfigToDataMap['hasRoyalty'] : {}) & + (C['collection'] extends true ? ConfigToDataMap['collection'] : {}); + +export type WithCreateTokenPolicy< + C extends ICreateTokenPolicyConfig, + Base, +> = Base & + (PolicyDataForConfig | undefined) & { + policyConfig?: C; + }; + +export const validatePolicies = ( + policyConfig?: ICreateTokenPolicyConfig, + policies: string[] = [], +) => { + if (policyConfig?.collection) { + if (!policies.includes(COLLECTION_POLICY)) { + throw new Error('Collection policy is required'); + } + } + + if (policyConfig?.guarded || policyConfig?.upgradeableURI) { + if (!policies.includes(GUARD_POLICY)) { + throw new Error('Guard policy is required'); + } + } + + if (policyConfig?.hasRoyalty) { + if (!policies.includes(ROYALTY_POLICY)) { + throw new Error('Royalty policy is required'); + } + } + + if (policyConfig?.nonFungible) { + if (!policies.includes(NON_FUNGIBLE_POLICY)) { + throw new Error('Non-fungible policy is required'); + } + } + + if (new Set(policies).size !== policies.length) { + throw new Error('Duplicate policies are not allowed'); + } +}; diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts index 977187db76..d6ef0aa151 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -12,6 +12,10 @@ import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; interface ITransferCreateTokenInput { + policyConfig?: { + guarded?: boolean; + hasRoyalty?: boolean; + }; tokenId: string; chainId: ChainId; sender: { @@ -37,8 +41,13 @@ const createTransferTokenCommand = ({ sender, receiver, amount, -}: ITransferCreateTokenInput) => - composePactCommand( + policyConfig, +}: ITransferCreateTokenInput) => { + if (policyConfig?.hasRoyalty) { + throw new Error('Royalty tokens cannot be transferred.'); + } + + return composePactCommand( execution( Pact.modules['marmalade-v2.ledger']['transfer-create']( tokenId, @@ -58,9 +67,21 @@ const createTransferTokenCommand = ({ receiver.account, amount, ), + ...(!!policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.TRANSFER', + tokenId, + sender.account, + receiver.account, + amount, + ), + ] + : []), ]), setMeta({ senderAccount: sender.account, chainId }), ); +}; export const transferCreateToken = ( inputs: ITransferCreateTokenInput, diff --git a/packages/libs/client-utils/src/marmalade/transfer-token.ts b/packages/libs/client-utils/src/marmalade/transfer-token.ts new file mode 100644 index 0000000000..8b78d53fbd --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/transfer-token.ts @@ -0,0 +1,85 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId, IPactDecimal } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface ITransferTokenInput { + policyConfig?: { + guarded?: boolean; + hasRoyalty?: boolean; + }; + tokenId: string; + chainId: ChainId; + sender: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + receiver: { + account: string; + }; + amount: IPactDecimal; +} + +const transferTokenCommand = ({ + tokenId, + chainId, + sender, + receiver, + amount, + policyConfig, +}: ITransferTokenInput) => { + if (policyConfig?.hasRoyalty) { + throw new Error('Royalty tokens cannot be transferred.'); + } + + return composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['transfer']( + tokenId, + sender.account, + receiver.account, + amount, + ), + ), + addSigner(sender.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + 'marmalade-v2.ledger.TRANSFER', + tokenId, + sender.account, + receiver.account, + amount, + ), + ...(!!policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.TRANSFER', + tokenId, + sender.account, + receiver.account, + amount, + ), + ] + : []), + ]), + setMeta({ senderAccount: sender.account, chainId }), + ); +}; + +export const transferToken = ( + inputs: ITransferTokenInput, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(transferTokenCommand(inputs)); From 03076126cec3ddc7ce92c36447631262c76573f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Maga=C5=A1?= Date: Mon, 6 May 2024 14:22:06 +0200 Subject: [PATCH 25/28] chore: add update-uri function --- .../integration-tests/marmalade.int.test.ts | 45 +++++++++++++ .../libs/client-utils/src/marmalade/index.ts | 3 +- .../client-utils/src/marmalade/update-uri.ts | 63 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/libs/client-utils/src/marmalade/update-uri.ts diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 580bceb785..f9062f599f 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -10,6 +10,7 @@ import { getAccountDetails, getTokenInfo, getUri, + updateUri, } from '../marmalade'; import { createToken } from '../marmalade/create-token'; import { createTokenId } from '../marmalade/create-token-id'; @@ -422,6 +423,50 @@ describe('getTokenUri', () => { }); }); +describe('updateUri', () => { + it('should update the uri', async () => { + const result = await updateUri( + { + tokenId: tokenId as string, + uri: "ipfs://updated-uri", + chainId, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ).execute(); + + expect(result).toBe(true); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = updateUri( + { + tokenId: nonExistingTokenId, + uri: "ipfs://updated-uri", + chainId, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error(`with-read: row not found: ${nonExistingTokenId}`), + ); + }); +}); + describe('getAccountDetails', () => { it('should get the account details', async () => { const result = await getAccountDetails( diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index 40c01e8d9f..d74181246b 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -1,6 +1,6 @@ export * from './burn-token'; -export * from './create-token'; export * from './create-token-id'; +export * from './create-token'; export * from './get-account-details'; export * from './get-token-balance'; export * from './get-token-info'; @@ -9,3 +9,4 @@ export * from './mint-token'; export * from './policy-config'; export * from './transfer-create-token'; export * from './transfer-token'; +export * from './update-uri'; diff --git a/packages/libs/client-utils/src/marmalade/update-uri.ts b/packages/libs/client-utils/src/marmalade/update-uri.ts new file mode 100644 index 0000000000..06febcda10 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/update-uri.ts @@ -0,0 +1,63 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IMintTokenInput { + policyConfig?: { + guarded?: boolean; + }; + tokenId: string; + uri: string; + chainId: ChainId; + guard: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +const updateUriCommand = ({ + tokenId, + uri, + chainId, + guard, + policyConfig, +}: IMintTokenInput) => composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger']['update-uri']( + tokenId, + uri, + ), + ), + addKeyset('guard', guard.keyset.pred, ...guard.keyset.keys), + addSigner(guard.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor('marmalade-v2.ledger.UPDATE-URI', tokenId, uri), + ...(!!policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.UPDATE-URI', + tokenId, + uri + ), + ] + : []), + ]), + setMeta({ senderAccount: guard.account, chainId }), + ); + +export const updateUri = (inputs: IMintTokenInput, config: IClientConfig) => + submitClient>( + config, + )(updateUriCommand(inputs)); From c187573f8a10f763233fea90602dddf5dccdb17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Maga=C5=A1?= Date: Mon, 6 May 2024 14:37:37 +0200 Subject: [PATCH 26/28] chore: format files --- .../integration-tests/marmalade.int.test.ts | 4 +-- .../client-utils/src/marmalade/burn-token.ts | 2 +- .../src/marmalade/get-account-details.ts | 2 +- .../libs/client-utils/src/marmalade/index.ts | 2 +- .../client-utils/src/marmalade/mint-token.ts | 2 +- .../src/marmalade/policy-config.ts | 8 +++--- .../src/marmalade/transfer-create-token.ts | 2 +- .../src/marmalade/transfer-token.ts | 4 +-- .../client-utils/src/marmalade/update-uri.ts | 26 ++++++------------- 9 files changed, 21 insertions(+), 31 deletions(-) diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index f9062f599f..7ae9ad4560 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -428,7 +428,7 @@ describe('updateUri', () => { const result = await updateUri( { tokenId: tokenId as string, - uri: "ipfs://updated-uri", + uri: 'ipfs://updated-uri', chainId, guard: { account: sourceAccount.account, @@ -448,7 +448,7 @@ describe('updateUri', () => { const task = updateUri( { tokenId: nonExistingTokenId, - uri: "ipfs://updated-uri", + uri: 'ipfs://updated-uri', chainId, guard: { account: sourceAccount.account, diff --git a/packages/libs/client-utils/src/marmalade/burn-token.ts b/packages/libs/client-utils/src/marmalade/burn-token.ts index 41963d6708..ae62842db2 100644 --- a/packages/libs/client-utils/src/marmalade/burn-token.ts +++ b/packages/libs/client-utils/src/marmalade/burn-token.ts @@ -56,7 +56,7 @@ const burnTokenCommand = ({ addSigner(guard.keyset.keys, (signFor) => [ signFor('coin.GAS'), signFor('marmalade-v2.ledger.BURN', tokenId, accountName, amount), - ...(!!policyConfig?.guarded + ...(policyConfig?.guarded ? [ signFor( 'marmalade-v2.guard-policy-v1.BURN', diff --git a/packages/libs/client-utils/src/marmalade/get-account-details.ts b/packages/libs/client-utils/src/marmalade/get-account-details.ts index cc757a3e95..1a20643da4 100644 --- a/packages/libs/client-utils/src/marmalade/get-account-details.ts +++ b/packages/libs/client-utils/src/marmalade/get-account-details.ts @@ -35,7 +35,7 @@ const getAccountDetailsCommand = ({ }: IGetAccountBalanceInput) => composePactCommand( execution( - Pact.modules['marmalade-v2.ledger']['details'](tokenId, accountName), + Pact.modules['marmalade-v2.ledger'].details(tokenId, accountName), ), addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), setMeta({ diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index d74181246b..fa2a7a1b03 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -1,6 +1,6 @@ export * from './burn-token'; -export * from './create-token-id'; export * from './create-token'; +export * from './create-token-id'; export * from './get-account-details'; export * from './get-token-balance'; export * from './get-token-info'; diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts index 8201f2a811..5b9c19c14b 100644 --- a/packages/libs/client-utils/src/marmalade/mint-token.ts +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -56,7 +56,7 @@ const mintTokenCommand = ({ addSigner(guard.keyset.keys, (signFor) => [ signFor('coin.GAS'), signFor('marmalade-v2.ledger.MINT', tokenId, accountName, amount), - ...(!!policyConfig?.guarded + ...(policyConfig?.guarded ? [ signFor( 'marmalade-v2.guard-policy-v1.MINT', diff --git a/packages/libs/client-utils/src/marmalade/policy-config.ts b/packages/libs/client-utils/src/marmalade/policy-config.ts index b5404908c4..03ad7f2dae 100644 --- a/packages/libs/client-utils/src/marmalade/policy-config.ts +++ b/packages/libs/client-utils/src/marmalade/policy-config.ts @@ -51,21 +51,21 @@ export const NON_FUNGIBLE_POLICY = 'non-fungible-policy-v1'; export const ROYALTY_POLICY = 'royalty-policy-v1'; export const COLLECTION_POLICY = 'collection-policy-v1'; -type ConfigToDataMap = { +interface ConfigToDataMap { customPolicies: { customPolicyData: Record }; upgradeableURI: {}; guarded: { guards: IGuardInfoInput }; nonFungible: {}; hasRoyalty: { royalty: IRoyaltyInfoInput }; collection: { collection: ICollectionInfoInput }; -}; +} -export type PolicyProps = { +export interface PolicyProps { customPolicyData: Record; guards: IGuardInfoInput; royalty: IRoyaltyInfoInput; collection: ICollectionInfoInput; -}; +} type PolicyDataForConfig = (C['customPolicies'] extends true ? ConfigToDataMap['customPolicies'] : {}) & diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts index d6ef0aa151..004ab34a58 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -67,7 +67,7 @@ const createTransferTokenCommand = ({ receiver.account, amount, ), - ...(!!policyConfig?.guarded + ...(policyConfig?.guarded ? [ signFor( 'marmalade-v2.guard-policy-v1.TRANSFER', diff --git a/packages/libs/client-utils/src/marmalade/transfer-token.ts b/packages/libs/client-utils/src/marmalade/transfer-token.ts index 8b78d53fbd..b85a325a78 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-token.ts @@ -44,7 +44,7 @@ const transferTokenCommand = ({ return composePactCommand( execution( - Pact.modules['marmalade-v2.ledger']['transfer']( + Pact.modules['marmalade-v2.ledger'].transfer( tokenId, sender.account, receiver.account, @@ -60,7 +60,7 @@ const transferTokenCommand = ({ receiver.account, amount, ), - ...(!!policyConfig?.guarded + ...(policyConfig?.guarded ? [ signFor( 'marmalade-v2.guard-policy-v1.TRANSFER', diff --git a/packages/libs/client-utils/src/marmalade/update-uri.ts b/packages/libs/client-utils/src/marmalade/update-uri.ts index 06febcda10..d853c18ac1 100644 --- a/packages/libs/client-utils/src/marmalade/update-uri.ts +++ b/packages/libs/client-utils/src/marmalade/update-uri.ts @@ -33,31 +33,21 @@ const updateUriCommand = ({ chainId, guard, policyConfig, -}: IMintTokenInput) => composePactCommand( - execution( - Pact.modules['marmalade-v2.ledger']['update-uri']( - tokenId, - uri, - ), - ), +}: IMintTokenInput) => + composePactCommand( + execution(Pact.modules['marmalade-v2.ledger']['update-uri'](tokenId, uri)), addKeyset('guard', guard.keyset.pred, ...guard.keyset.keys), addSigner(guard.keyset.keys, (signFor) => [ signFor('coin.GAS'), signFor('marmalade-v2.ledger.UPDATE-URI', tokenId, uri), - ...(!!policyConfig?.guarded - ? [ - signFor( - 'marmalade-v2.guard-policy-v1.UPDATE-URI', - tokenId, - uri - ), - ] + ...(policyConfig?.guarded + ? [signFor('marmalade-v2.guard-policy-v1.UPDATE-URI', tokenId, uri)] : []), ]), setMeta({ senderAccount: guard.account, chainId }), ); export const updateUri = (inputs: IMintTokenInput, config: IClientConfig) => - submitClient>( - config, - )(updateUriCommand(inputs)); + submitClient< + PactReturnType + >(config)(updateUriCommand(inputs)); From b1d964fa7830367b3d20e40cbb33bbe6abbe4049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Maga=C5=A1?= Date: Thu, 23 May 2024 14:56:26 +0200 Subject: [PATCH 27/28] [@kadena/client-utils] Implement Marmalade functions - Sale functions (#2055) * chore: added more maramalade util functions * chore: align create-token with other functions * fix: linting issues * feat: added sale functions and tests; chore: updated and cleaned read functions * chore: linting * chore: added script to setup test environment before executing integration tests * chore: lint setup marmalade test env file * chore: gnerate marmalade-v2.policy-manager types * chore: check if conventional and dutch auction have been whitelisted * chore: remove whitelisting sale from test setup script * chore: wait for block time helper function * chore: fix lint issue * chore: enforce failure uri guard if not updatable when using guard policy * chore: rename config to updatableURI * chore: remove guard on local calls * feat: added collection-policy functions * chore: adjust the auction end date in tests * chore: cleanup and stability improvements * fix: more time * chore: simplify local call functions * chore: linting --- packages/libs/client-utils/package.json | 6 +- .../marmalade-collection-policy.int.test.ts | 376 +++++++ ...marmalade-conventional-auction.int.test.ts | 976 ++++++++++++++++++ .../marmalade-dutch-auction.int.test.ts | 818 +++++++++++++++ .../marmalade-gas-station.int.test.ts | 250 +++++ .../marmalade-sale.int.test.ts | 644 ++++++++++++ .../integration-tests/marmalade.int.test.ts | 457 ++++---- .../src/integration-tests/support/helpers.ts | 164 +++ .../client-utils/src/marmalade/burn-token.ts | 14 +- .../client-utils/src/marmalade/buy-token.ts | 165 +++ .../libs/client-utils/src/marmalade/config.ts | 331 ++++++ .../src/marmalade/create-auction.ts | 126 +++ .../src/marmalade/create-bid-id.ts | 42 + .../src/marmalade/create-collection-id.ts | 53 + .../src/marmalade/create-collection.ts | 75 ++ .../src/marmalade/create-token-id.ts | 58 +- .../src/marmalade/create-token.ts | 94 +- .../src/marmalade/escrow-account.ts | 39 + .../src/marmalade/get-account-details.ts | 64 +- .../src/marmalade/get-auction-details.ts | 62 ++ .../client-utils/src/marmalade/get-bid.ts | 34 + .../src/marmalade/get-collection-token.ts | 37 + .../src/marmalade/get-collection.ts | 39 + .../src/marmalade/get-current-price.ts | 37 + .../src/marmalade/get-escrow-account.ts | 37 + .../src/marmalade/get-token-balance.ts | 66 +- .../src/marmalade/get-token-info.ts | 61 +- .../client-utils/src/marmalade/get-uri.ts | 53 +- .../client-utils/src/marmalade/helpers.ts | 40 + .../libs/client-utils/src/marmalade/index.ts | 17 +- .../client-utils/src/marmalade/mint-token.ts | 14 +- .../client-utils/src/marmalade/offer-token.ts | 121 +++ .../client-utils/src/marmalade/place-bid.ts | 87 ++ .../src/marmalade/policy-config.ts | 11 +- .../src/marmalade/transfer-create-token.ts | 14 +- .../src/marmalade/transfer-token.ts | 14 +- .../src/marmalade/update-auction.ts | 124 +++ .../src/marmalade/withdraw-token.ts | 93 ++ .../src/scripts/setup-marmalade-test-env.ts | 93 ++ .../client-utils/vitest.integration.config.ts | 2 +- 40 files changed, 5322 insertions(+), 486 deletions(-) create mode 100644 packages/libs/client-utils/src/integration-tests/marmalade-collection-policy.int.test.ts create mode 100644 packages/libs/client-utils/src/integration-tests/marmalade-conventional-auction.int.test.ts create mode 100644 packages/libs/client-utils/src/integration-tests/marmalade-dutch-auction.int.test.ts create mode 100644 packages/libs/client-utils/src/integration-tests/marmalade-gas-station.int.test.ts create mode 100644 packages/libs/client-utils/src/integration-tests/marmalade-sale.int.test.ts create mode 100644 packages/libs/client-utils/src/marmalade/buy-token.ts create mode 100644 packages/libs/client-utils/src/marmalade/config.ts create mode 100644 packages/libs/client-utils/src/marmalade/create-auction.ts create mode 100644 packages/libs/client-utils/src/marmalade/create-bid-id.ts create mode 100644 packages/libs/client-utils/src/marmalade/create-collection-id.ts create mode 100644 packages/libs/client-utils/src/marmalade/create-collection.ts create mode 100644 packages/libs/client-utils/src/marmalade/escrow-account.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-auction-details.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-bid.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-collection-token.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-collection.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-current-price.ts create mode 100644 packages/libs/client-utils/src/marmalade/get-escrow-account.ts create mode 100644 packages/libs/client-utils/src/marmalade/helpers.ts create mode 100644 packages/libs/client-utils/src/marmalade/offer-token.ts create mode 100644 packages/libs/client-utils/src/marmalade/place-bid.ts create mode 100644 packages/libs/client-utils/src/marmalade/update-auction.ts create mode 100644 packages/libs/client-utils/src/marmalade/withdraw-token.ts create mode 100644 packages/libs/client-utils/src/scripts/setup-marmalade-test-env.ts diff --git a/packages/libs/client-utils/package.json b/packages/libs/client-utils/package.json index 8910a74cd9..db3668fc06 100644 --- a/packages/libs/client-utils/package.json +++ b/packages/libs/client-utils/package.json @@ -113,9 +113,11 @@ "lint:fmt": "prettier . --cache --check", "lint:pkg": "lint-package", "lint:src": "eslint src --ext .js,.ts", - "pactjs:generate:contract": "pactjs contract-generate --contract coin --contract marmalade-v2.ledger --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact", + "pactjs:generate:contract": "pactjs contract-generate --contract coin --contract marmalade-v2.ledger --contract marmalade-v2.collection-policy-v1 --contract marmalade-v2.policy-manager --contract marmalade-sale.conventional-auction --contract marmalade-sale.dutch-auction --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact", "test": "vitest run", - "test:integration": "vitest run -c ./vitest.integration.config.ts", + "test:integration": "pnpm run test:integration:setup && vitest run -c ./vitest.integration.config.ts", + "test:integration:local": "vitest run -c ./vitest.integration.config.ts", + "test:integration:setup": "pnpm run build && ts-node src/scripts/setup-marmalade-test-env.ts", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/libs/client-utils/src/integration-tests/marmalade-collection-policy.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade-collection-policy.int.test.ts new file mode 100644 index 0000000000..453e809d35 --- /dev/null +++ b/packages/libs/client-utils/src/integration-tests/marmalade-collection-policy.int.test.ts @@ -0,0 +1,376 @@ +import type { ChainId } from '@kadena/client'; +import { createSignWithKeypair } from '@kadena/client'; +import { PactNumber } from '@kadena/pactjs'; +import type { IPactInt } from '@kadena/types'; +import { describe, expect, it } from 'vitest'; +import { + createCollection, + createCollectionId, + createToken, + createTokenId, + getCollection, + getCollectionToken, + getTokenBalance, + mintToken, +} from '../marmalade'; +import type { ICreateTokenPolicyConfig } from '../marmalade/config'; +import { NetworkIds } from './support/NetworkIds'; +import { withStepFactory } from './support/helpers'; +import { sourceAccount } from './test-data/accounts'; + +let tokenId: string | undefined; +let collectionId: string | undefined; +let collectionName: string | undefined; +const collectionSize: IPactInt = { int: '0' }; +const chainId = '0' as ChainId; + +const inputs = { + policyConfig: { + collection: true, + } as ICreateTokenPolicyConfig, + collection: { + collectionId, + }, + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: ['marmalade-v2.collection-policy-v1'], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, +}; +const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), +}; + +describe('createCollectionId', () => { + it('should return a collection id', async () => { + collectionName = `Test Collection #${Math.random().toString()}`; + + collectionId = await createCollectionId({ + collectionName, + chainId, + operator: { + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(collectionId).toBeDefined(); + expect(collectionId).toMatch(/^collection:.{43}$/); + + inputs.collection.collectionId = collectionId; + }); +}); + +describe('createCollection', () => { + it('should create a collection', async () => { + const withStep = withStepFactory(); + + const result = await createCollection( + { + id: collectionId as string, + name: collectionName as string, + size: collectionSize, + operator: inputs.creator, + chainId, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('createTokenId', () => { + it('should return a token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); +}); + +describe('createToken', () => { + it('should create a token with policy', async () => { + const withStep = withStepFactory(); + + const tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + + const result = await createToken( + { + ...inputs, + tokenId: tokenId as string, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('mintToken', () => { + it('should mint a token', async () => { + const withStep = withStepFactory(); + + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + chainId, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); + it('should throw error when non-existent token is minted', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = mintToken( + { + ...inputs, + tokenId: nonExistingTokenId, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('with-read: row not found: non-existing-token'), + ); + }); +}); + +describe('getCollection', () => { + it('should get the collection details', async () => { + const result = await getCollection({ + chainId, + collectionId: collectionId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toStrictEqual({ + id: collectionId, + 'max-size': { + int: Number(collectionSize.int), + }, + name: collectionName, + 'operator-guard': inputs.creator.keyset, + size: { + int: 1, + }, + }); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-collection'; + const task = getCollection({ + collectionId: nonExistingTokenId, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + await expect(() => Promise.resolve(task)).rejects.toThrowError( + new Error(`read: row not found: non-existing-collection`), + ); + }); +}); + +describe('getCollectionToken', () => { + it('should get the collection token details', async () => { + const result = await getCollectionToken({ + chainId, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toStrictEqual({ + id: tokenId, + 'collection-id': collectionId, + }); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = getCollectionToken({ + chainId, + tokenId: nonExistingTokenId, + networkId: config.defaults.networkId, + host: config.host, + }); + + await expect(() => Promise.resolve(task)).rejects.toThrowError( + new Error(`read: row not found: non-existing-token`), + ); + }); +}); diff --git a/packages/libs/client-utils/src/integration-tests/marmalade-conventional-auction.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade-conventional-auction.int.test.ts new file mode 100644 index 0000000000..7289797bae --- /dev/null +++ b/packages/libs/client-utils/src/integration-tests/marmalade-conventional-auction.int.test.ts @@ -0,0 +1,976 @@ +import type { ChainId } from '@kadena/client'; +import { createSignWithKeypair } from '@kadena/client'; +import { PactNumber } from '@kadena/pactjs'; +import type { IPactInt } from '@kadena/types'; +import { describe, expect, it } from 'vitest'; +import { + buyToken, + createAuction, + createBidId, + createToken, + createTokenId, + escrowAccount, + getAuctionDetails, + getBid, + getTokenBalance, + mintToken, + offerToken, + placeBid, + updateAuction, +} from '../marmalade'; +import { NetworkIds } from './support/NetworkIds'; +import { + addDaysToDate, + addMinutesToDate, + addSecondsToDate, + dateToPactInt, + waitForBlockTime, + withStepFactory, +} from './support/helpers'; +import { secondaryTargetAccount, sourceAccount } from './test-data/accounts'; + +let tokenId: string | undefined; +let saleId: string | undefined; +let bidId: string | undefined; +const timeout = dateToPactInt(addDaysToDate(new Date(), 1)); +let auctionStartDate: IPactInt; +let auctionEndDate: IPactInt; +const chainId = '0' as ChainId; +const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, +}; +const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), +}; + +describe('createTokenId', () => { + it('should return a token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); +}); + +describe('createToken', () => { + it('should create a token', async () => { + const withStep = withStepFactory(); + + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('mintToken', () => { + it('should mint a token', async () => { + const withStep = withStepFactory(); + + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); + +describe('offerToken - with auction data', () => { + it('should offer a token for sale', async () => { + const withStep = withStepFactory(); + + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), + }; + + const result = await offerToken( + { + policyConfig: { + auction: true, + }, + auction: { + fungible: { + refName: { + name: 'coin', + namespace: null, + }, + refSpec: [ + { + name: 'fungible-v2', + namespace: null, + }, + ], + }, + sellerFungibleAccount: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + price: { decimal: '0.0' }, + saleType: 'marmalade-sale.conventional-auction', + }, + chainId, + tokenId: tokenId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + saleConfig, + ) + .on( + 'sign', + withStep((step, tx) => { + saleId = tx.hash; + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + }); + + it('should throw error when non-existent token offered', async () => { + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const task = await offerToken( + { + chainId, + tokenId: 'non-existing-token', + seller: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout: new PactNumber( + Math.floor(addMinutesToDate(new Date(), 1).getTime() / 1000), + ).toPactInteger(), + }, + saleConfig, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('with-read: row not found: non-existing-token'), + ); + }); +}); + +describe('createAuction', () => { + it('should be able to create conventional auction', async () => { + const withStep = withStepFactory(); + + const result = await createAuction( + { + auctionConfig: { + conventional: true, + }, + saleId: saleId as string, + tokenId: tokenId as string, + startDate: dateToPactInt(addSecondsToDate(new Date(), 50)), + endDate: dateToPactInt(addMinutesToDate(new Date(), 60)), + reservedPrice: { decimal: '1.0' }, + chainId, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('updateAuction', () => { + it('should be able to update conventional auction', async () => { + const withStep = withStepFactory(); + + auctionStartDate = dateToPactInt(addSecondsToDate(new Date(), 10)); + auctionEndDate = dateToPactInt(addMinutesToDate(new Date(), 10)); + + const result = await updateAuction( + { + auctionConfig: { + conventional: true, + }, + saleId: saleId as string, + tokenId: tokenId as string, + startDate: auctionStartDate, + endDate: auctionEndDate, + reservedPrice: { decimal: '1.5' }, + chainId, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe('Write succeeded'); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe('Write succeeded'); + } + }), + ) + .execute(); + + expect(result).toBe('Write succeeded'); + }); +}); + +describe('getAuctionDetails', () => { + it('should get the auction details', async () => { + const result = await getAuctionDetails({ + auctionConfig: { + conventional: true, + }, + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toStrictEqual( + expect.objectContaining({ + 'token-id': tokenId, + 'reserve-price': 1.5, + 'highest-bid': 0, + 'highest-bid-id': '', + }), + ); + }); +}); + +describe('placeBid', () => { + it('should be able to place a bid on conventional auction', async () => { + await waitForBlockTime((Number(auctionStartDate.int) + 2) * 1000); + + const withStep = withStepFactory(); + + const _escrowAccount = await escrowAccount({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + const result = await placeBid( + { + bid: { decimal: '2.0' }, + bidder: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + escrowAccount: _escrowAccount as string, + saleId: saleId as string, + chainId, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + const bidEvent = sbResult.events?.find( + (event) => event.name === 'BID_PLACED', + ); + + bidId = bidEvent?.params[0] as string; + + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('createBidId', () => { + it('should return a bid id', async () => { + const bidId = await createBidId({ + saleId: saleId as string, + bidderAccount: sourceAccount.account, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(bidId).toBeDefined(); + }); +}); + +describe('getBid', () => { + it('should get the bid', async () => { + const result = await getBid({ + bidId: bidId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toStrictEqual({ + bid: 2, + bidder: sourceAccount.account, + 'bidder-guard': { + keys: [sourceAccount.publicKey], + pred: 'keys-all', + }, + }); + }); +}); + +describe('buyToken', () => { + const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }; + let auctionStartDate: IPactInt; + let auctionEndDate: IPactInt; + + it('should create token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); + + it('should create a token', async () => { + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should mint a token', async () => { + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should offer a token for sale', async () => { + const withStep = withStepFactory(); + + const result = await offerToken( + { + policyConfig: { + auction: true, + }, + auction: { + fungible: { + refName: { + name: 'coin', + namespace: null, + }, + refSpec: [ + { + name: 'fungible-v2', + namespace: null, + }, + ], + }, + sellerFungibleAccount: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + price: { decimal: '0.0' }, + saleType: 'marmalade-sale.conventional-auction', + }, + chainId, + tokenId: tokenId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + saleId = tx.hash; + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + }); + + it('should create conventional auction', async () => { + auctionStartDate = dateToPactInt(addSecondsToDate(new Date(), 10)); + auctionEndDate = dateToPactInt(addSecondsToDate(new Date(), 80)); + + const result = await createAuction( + { + auctionConfig: { + conventional: true, + }, + saleId: saleId as string, + tokenId: tokenId as string, + startDate: auctionStartDate, + endDate: auctionEndDate, + reservedPrice: { decimal: '1.0' }, + chainId, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should be able to place a bid on conventional auction', async () => { + await waitForBlockTime((Number(auctionStartDate.int) + 2) * 1000); + + const withStep = withStepFactory(); + + const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const _escrowAccount = await escrowAccount({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + const result = await placeBid( + { + bid: { decimal: '2.0' }, + bidder: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + escrowAccount: _escrowAccount as string, + saleId: saleId as string, + chainId, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + const bidEvent = sbResult.events?.find( + (event) => event.name === 'BID_PLACED', + ); + + bidId = bidEvent?.params[0] as string; + + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); + + it('should buy a token', async () => { + await waitForBlockTime((Number(auctionEndDate.int) + 7) * 1000); + + const withStep = withStepFactory(); + + const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const _escrowAccount = await escrowAccount({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(_escrowAccount).toBeDefined(); + + const result = await buyToken( + { + auctionConfig: { + conventional: true, + }, + escrow: { account: _escrowAccount as string }, + updatedPrice: { decimal: '2.0' }, + chainId, + tokenId: tokenId as string, + saleId: saleId as string, + seller: { + account: sourceAccount.account, + }, + buyer: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + + const balance = await getTokenBalance({ + accountName: secondaryTargetAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); diff --git a/packages/libs/client-utils/src/integration-tests/marmalade-dutch-auction.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade-dutch-auction.int.test.ts new file mode 100644 index 0000000000..72230c28bf --- /dev/null +++ b/packages/libs/client-utils/src/integration-tests/marmalade-dutch-auction.int.test.ts @@ -0,0 +1,818 @@ +import type { ChainId } from '@kadena/client'; +import { createSignWithKeypair } from '@kadena/client'; +import { PactNumber } from '@kadena/pactjs'; +import type { IPactInt } from '@kadena/types'; +import { describe, expect, it } from 'vitest'; +import { + buyToken, + createAuction, + createToken, + createTokenId, + getAuctionDetails, + getCurrentPrice, + getEscrowAccount, + getTokenBalance, + mintToken, + offerToken, + updateAuction, +} from '../marmalade'; +import { NetworkIds } from './support/NetworkIds'; +import { + addDaysToDate, + addSecondsToDate, + dateToPactInt, + waitForBlockTime, + withStepFactory, +} from './support/helpers'; +import { secondaryTargetAccount, sourceAccount } from './test-data/accounts'; + +let tokenId: string | undefined; +let saleId: string | undefined; +const timeout = dateToPactInt(addDaysToDate(new Date(), 1)); +let auctionStartDate: IPactInt; +let auctionEndDate: IPactInt; +const chainId = '0' as ChainId; +const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, +}; +const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), +}; + +describe('createTokenId', () => { + it('should return a token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); +}); + +describe('createToken', () => { + it('should create a token', async () => { + const withStep = withStepFactory(); + + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('mintToken', () => { + it('should mint a token', async () => { + const withStep = withStepFactory(); + + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + chainId, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); + +describe('offerToken - with auction data', () => { + it('should offer a token for sale', async () => { + const withStep = withStepFactory(); + + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), + }; + + const result = await offerToken( + { + policyConfig: { + auction: true, + }, + auction: { + fungible: { + refName: { + name: 'coin', + namespace: null, + }, + refSpec: [ + { + name: 'fungible-v2', + namespace: null, + }, + ], + }, + sellerFungibleAccount: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + price: { decimal: '0.0' }, + saleType: 'marmalade-sale.dutch-auction', + }, + chainId, + tokenId: tokenId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + saleConfig, + ) + .on( + 'sign', + withStep((step, tx) => { + saleId = tx.hash; + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + }); + + it('should throw error when non-existent token offered', async () => { + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const task = await offerToken( + { + chainId, + tokenId: 'non-existing-token', + seller: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + saleConfig, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('with-read: row not found: non-existing-token'), + ); + }); +}); + +describe('createAuction', () => { + it('should be able to create dutch auction', async () => { + const withStep = withStepFactory(); + + const result = await createAuction( + { + auctionConfig: { + dutch: true, + }, + saleId: saleId as string, + tokenId: tokenId as string, + startDate: dateToPactInt(addSecondsToDate(new Date(), 50)), + endDate: dateToPactInt(addSecondsToDate(new Date(), 100)), + startPrice: { decimal: '100.0' }, + reservedPrice: { decimal: '1.0' }, + priceIntervalInSeconds: { int: '10' }, + chainId, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('updateAuction', () => { + it('should be able to update dutch auction', async () => { + const withStep = withStepFactory(); + + auctionStartDate = dateToPactInt(addSecondsToDate(new Date(), 10)); + auctionEndDate = dateToPactInt(addDaysToDate(new Date(), 10)); + + const result = await updateAuction( + { + auctionConfig: { + dutch: true, + }, + saleId: saleId as string, + tokenId: tokenId as string, + startDate: auctionStartDate, + endDate: auctionEndDate, + startPrice: { decimal: '10.0' }, + reservedPrice: { decimal: '1.0' }, + priceIntervalInSeconds: { int: '10' }, + chainId, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe('Write succeeded'); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe('Write succeeded'); + } + }), + ) + .execute(); + + expect(result).toBe('Write succeeded'); + }); +}); + +describe('getAuctionDetails', () => { + it('should get the auction details', async () => { + const result = await getAuctionDetails({ + auctionConfig: { + dutch: true, + }, + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toStrictEqual( + expect.objectContaining({ + buyer: '', + 'price-interval-seconds': { + int: 10, + }, + 'reserve-price': 1, + 'sell-price': 0, + 'start-price': 10, + }), + ); + }); +}); + +describe('getCurrentPrice', () => { + it('should return 0 if the auction has not started yet', async () => { + const result = await getCurrentPrice({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toBe(0); + }); + + it('should return start price after the auction have started', async () => { + await waitForBlockTime((Number(auctionStartDate.int) + 2) * 1000); + + const result = await getCurrentPrice({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toBe(10); + }); +}); + +describe('buyToken', () => { + const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }; + + let auctionStartDate: IPactInt; + let auctionEndDate: IPactInt; + + it('should create token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); + + it('should create a token', async () => { + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should mint a token', async () => { + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should offer a token for sale', async () => { + const withStep = withStepFactory(); + + const result = await offerToken( + { + policyConfig: { + auction: true, + }, + auction: { + fungible: { + refName: { + name: 'coin', + namespace: null, + }, + refSpec: [ + { + name: 'fungible-v2', + namespace: null, + }, + ], + }, + sellerFungibleAccount: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + price: { decimal: '0.0' }, + saleType: 'marmalade-sale.dutch-auction', + }, + chainId, + tokenId: tokenId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + saleId = tx.hash; + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + }); + + it('should create dutch auction', async () => { + auctionStartDate = dateToPactInt(addSecondsToDate(new Date(), 10)); + auctionEndDate = dateToPactInt(addDaysToDate(new Date(), 10)); + + const result = await createAuction( + { + auctionConfig: { + dutch: true, + }, + saleId: saleId as string, + tokenId: tokenId as string, + startDate: auctionStartDate, + endDate: auctionEndDate, + startPrice: { decimal: '5.0' }, + reservedPrice: { decimal: '1.0' }, + priceIntervalInSeconds: { int: '3600' }, + chainId, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should buy a token', async () => { + await waitForBlockTime((Number(auctionStartDate.int) + 2) * 1000); + + const withStep = withStepFactory(); + + const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const escrowAccount = await getEscrowAccount({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(escrowAccount).toBeDefined(); + + const latestPrice = await getCurrentPrice({ + saleId: saleId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(latestPrice).toBeDefined(); + + const result = await buyToken( + { + auctionConfig: { + dutch: true, + }, + updatedPrice: { decimal: String(latestPrice) }, + escrow: { + account: (escrowAccount as any).account, + }, + chainId, + tokenId: tokenId as string, + saleId: saleId as string, + seller: { + account: sourceAccount.account, + }, + buyer: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + + const balance = await getTokenBalance({ + accountName: secondaryTargetAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); diff --git a/packages/libs/client-utils/src/integration-tests/marmalade-gas-station.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade-gas-station.int.test.ts new file mode 100644 index 0000000000..31212cef6f --- /dev/null +++ b/packages/libs/client-utils/src/integration-tests/marmalade-gas-station.int.test.ts @@ -0,0 +1,250 @@ +import type { ChainId } from '@kadena/client'; +import { createSignWithKeypair } from '@kadena/client'; +import { PactNumber } from '@kadena/pactjs'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { describeModule } from '../built-in'; +import { getBalance, transfer } from '../coin'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + createToken, + createTokenId, + getTokenBalance, + mintToken, +} from '../marmalade'; +import { NetworkIds } from './support/NetworkIds'; +import { deployGasStation, withStepFactory } from './support/helpers'; +import { sender00Account, sourceAccount } from './test-data/accounts'; + +let tokenId: string | undefined; +const chainId = '0' as ChainId; +const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, +}; +const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), +}; + +beforeAll(async () => { + const config: IClientConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + meta: { + chainId, + }, + }, + sign: createSignWithKeypair([sender00Account]), + }; + + let gasStationDeployed = false; + try { + await describeModule('free.test-gas-station', config); + gasStationDeployed = true; + } catch (error) { + console.log('Gas station not deployed, deploying now'); + } + + if (!gasStationDeployed) { + await deployGasStation({ + chainId, + }); + } + + const gasStationFunds = await getBalance( + 'test-gas-station', + config.defaults?.networkId as string, + chainId, + config.host, + ); + + if (gasStationFunds === '0') { + console.log('Gas station has no funds, topping up now'); + await transfer( + { + sender: { + account: sender00Account.account, + publicKeys: [sender00Account.publicKey], + }, + receiver: 'test-gas-station', + amount: '100', + chainId, + }, + config, + ).execute(); + } +}, 300000); + +describe('createTokenId', () => { + it('should return a token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); +}); + +describe('createToken', () => { + it('should create a token', async () => { + const withStep = withStepFactory(); + + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('mintToken', () => { + it('should mint a token using a gas station to sponsor the gas fee', async () => { + const withStep = withStepFactory(); + + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + meta: { + senderAccount: 'test-gas-station', + }, + capabilities: [ + { + name: 'free.test-gas-station.GAS_PAYER', + props: [sourceAccount.account, { int: 0 }, { decimal: '0.0' }], + }, + ], + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + + const transferEvent = prResult.events!.find( + (event) => event.name === 'TRANSFER', + )!; + + // ensure that the gas station payed for the gas fee + expect(transferEvent.params[0]).toBe('test-gas-station'); + expect(transferEvent.params[1]).toBe('NoMiner'); + expect(transferEvent.params[2]).toBe( + prResult.gas * prResult.metaData!.publicMeta!.gasPrice, + ); + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); diff --git a/packages/libs/client-utils/src/integration-tests/marmalade-sale.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade-sale.int.test.ts new file mode 100644 index 0000000000..1f5e3b869f --- /dev/null +++ b/packages/libs/client-utils/src/integration-tests/marmalade-sale.int.test.ts @@ -0,0 +1,644 @@ +import type { ChainId } from '@kadena/client'; +import { createSignWithKeypair } from '@kadena/client'; +import { PactNumber } from '@kadena/pactjs'; +import type { IPactInt } from '@kadena/types'; +import { describe, expect, it } from 'vitest'; +import { + buyToken, + createToken, + createTokenId, + getTokenBalance, + mintToken, + offerToken, + withdrawToken, +} from '../marmalade'; +import { NetworkIds } from './support/NetworkIds'; +import { + addMinutesToDate, + addSecondsToDate, + dateToPactInt, + waitForBlockTime, + withStepFactory, +} from './support/helpers'; +import { secondaryTargetAccount, sourceAccount } from './test-data/accounts'; + +let tokenId: string | undefined; +let saleId: string | undefined; +let escrowAccount: string | undefined; +let timeout: IPactInt; +const chainId = '0' as ChainId; +const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, +}; +const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), +}; + +describe('createTokenId', () => { + it('should return a token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); +}); + +describe('createToken', () => { + it('should create a token', async () => { + const withStep = withStepFactory(); + + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); +}); + +describe('mintToken', () => { + it('should mint a token', async () => { + const withStep = withStepFactory(); + + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + chainId, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); + +describe('offerToken - default', () => { + it('should offer a token for sale', async () => { + const withStep = withStepFactory(); + + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), + }; + + timeout = dateToPactInt(addSecondsToDate(new Date(), 30)); + + const result = await offerToken( + { + chainId, + tokenId: tokenId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + saleConfig, + ) + .on( + 'sign', + withStep((step, tx) => { + saleId = tx.hash; + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + const transfer = prResult.events?.find( + (event) => + event.name === 'TRANSFER' && event.module.name === 'ledger', + ); + + escrowAccount = transfer!.params[2].toString(); + + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + + const sellerBalance = await getTokenBalance({ + accountName: sourceAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(sellerBalance).toBe(0); + + const escrowBalance = await getTokenBalance({ + accountName: escrowAccount as string, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(escrowBalance).toBe(1); + }); + + it('should throw error when non-existent token offered', async () => { + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const task = offerToken( + { + chainId, + tokenId: 'non-existing-token', + seller: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout: new PactNumber( + Math.floor(addMinutesToDate(new Date(), 1).getTime() / 1000), + ).toPactInteger(), + }, + saleConfig, + ); + + await expect(() => task.execute()).rejects.toThrowError( + new Error('with-read: row not found: non-existing-token'), + ); + }); +}); + +describe('withdrawToken', () => { + it('should not be able to withdraw a token from the sale if still active', async () => { + const withStep = withStepFactory(); + + const result = withdrawToken( + { + chainId, + tokenId: tokenId as string, + saleId: saleId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'success') { + expect(prResult.result.status).toBe('failure'); + } else { + expect(prResult.result.error).toStrictEqual( + expect.objectContaining({ + message: 'WITHDRAW: still active', + }), + ); + } + }), + ); + + await expect(result.execute()).rejects.toStrictEqual( + expect.objectContaining({ + message: 'WITHDRAW: still active', + }), + ); + }); + + it('should withdraw a token from the sale after the timeout have passed', async () => { + // wait for the sale timeout to pass + await waitForBlockTime((Number(timeout.int) + 2) * 1000); + + const withStep = withStepFactory(); + + const result = await withdrawToken( + { + chainId, + tokenId: tokenId as string, + saleId: saleId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); + +describe('buyToken', () => { + const inputs = { + chainId, + precision: { int: '0' }, + uri: Math.random().toString(), + policies: [], + creator: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + }; + + it('should create token id', async () => { + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + }); + + it('should create a token', async () => { + const result = await createToken( + { ...inputs, tokenId: tokenId as string }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should mint a token', async () => { + const result = await mintToken( + { + ...inputs, + tokenId: tokenId as string, + accountName: sourceAccount.account, + guard: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + }, + config, + ).execute(); + + expect(result).toBe(true); + }); + + it('should offer a token for sale', async () => { + const withStep = withStepFactory(); + + const saleConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), + }; + + timeout = dateToPactInt(addSecondsToDate(new Date(), 30)); + + const result = await offerToken( + { + chainId, + tokenId: tokenId as string, + seller: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + saleConfig, + ) + .on( + 'sign', + withStep((step, tx) => { + saleId = tx.hash; + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .execute(); + + expect(result).toBe(saleId); + }); + + it('should buy a token', async () => { + // wait for the sale timeout to pass + await waitForBlockTime((Number(timeout.int) + 2) * 1000); + + const withStep = withStepFactory(); + + const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([secondaryTargetAccount]), + }; + + const result = await buyToken( + { + chainId, + tokenId: tokenId as string, + saleId: saleId as string, + seller: { + account: sourceAccount.account, + }, + buyer: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all' as const, + }, + }, + amount: new PactNumber(1).toPactDecimal(), + timeout, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(saleId); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(saleId); + } + }), + ) + .execute(); + + expect(result).toBe(saleId); + + const balance = await getTokenBalance({ + accountName: secondaryTargetAccount.account, + chainId, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(balance).toBe(1); + }); +}); diff --git a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts index 7ae9ad4560..926b9ab1bc 100644 --- a/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts +++ b/packages/libs/client-utils/src/integration-tests/marmalade.int.test.ts @@ -1,37 +1,29 @@ import type { ChainId } from '@kadena/client'; import { createSignWithKeypair } from '@kadena/client'; import { PactNumber } from '@kadena/pactjs'; -import { beforeAll, describe, expect, it } from 'vitest'; -import { describeModule } from '../built-in'; -import { transferCreate } from '../coin'; -import type { IClientConfig } from '../core/utils/helpers'; +import { describe, expect, it } from 'vitest'; import { burnToken, + createToken, + createTokenId, getAccountDetails, + getTokenBalance, getTokenInfo, getUri, + mintToken, + transferCreateToken, updateUri, } from '../marmalade'; -import { createToken } from '../marmalade/create-token'; -import { createTokenId } from '../marmalade/create-token-id'; -import { getTokenBalance } from '../marmalade/get-token-balance'; -import { mintToken } from '../marmalade/mint-token'; -import { transferCreateToken } from '../marmalade/transfer-create-token'; -import { deployMarmalade } from '../nodejs'; import { NetworkIds } from './support/NetworkIds'; import { withStepFactory } from './support/helpers'; -import { - secondaryTargetAccount, - sender00Account, - sourceAccount, -} from './test-data/accounts'; +import { secondaryTargetAccount, sourceAccount } from './test-data/accounts'; let tokenId: string | undefined; const chainId = '0' as ChainId; const inputs = { chainId, precision: { int: '0' }, - uri: Date.now().toString(), + uri: Math.random().toString(), policies: [], creator: { account: sourceAccount.account, @@ -49,79 +41,13 @@ const config = { sign: createSignWithKeypair([sourceAccount]), }; -beforeAll(async () => { - const fundConfig: IClientConfig = { - host: 'http://127.0.0.1:8080', - defaults: { - networkId: 'development', - meta: { - chainId, - }, - }, - sign: createSignWithKeypair([sender00Account]), - }; - let marmaladeDeployed = false; - - try { - await describeModule('marmalade-v2.ledger', fundConfig); - marmaladeDeployed = true; - } catch (error) { - console.log('Marmalade not deployed, deploying now'); - } - - if (!marmaladeDeployed) { - await deployMarmalade({ - chainIds: [chainId], - deleteFilesAfterDeployment: true, - }); - } - - const [resultSourceAccount, resultTargetAccount] = await Promise.all([ - transferCreate( - { - sender: { - account: sender00Account.account, - publicKeys: [sender00Account.publicKey], - }, - receiver: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all', - }, - }, - amount: '100', - chainId, - }, - fundConfig, - ).execute(), - transferCreate( - { - sender: { - account: sender00Account.account, - publicKeys: [sender00Account.publicKey], - }, - receiver: { - account: secondaryTargetAccount.account, - keyset: { - keys: [secondaryTargetAccount.publicKey], - pred: 'keys-all', - }, - }, - amount: '100', - chainId, - }, - fundConfig, - ).execute(), - ]); - - expect(resultSourceAccount).toBe('Write succeeded'); - expect(resultTargetAccount).toBe('Write succeeded'); -}, 300000); - describe('createTokenId', () => { it('should return a token id', async () => { - tokenId = await createTokenId(inputs, config).execute(); + tokenId = await createTokenId({ + ...inputs, + networkId: config.defaults.networkId, + host: config.host, + }); expect(tokenId).toBeDefined(); expect(tokenId).toMatch(/^t:.{43}$/); @@ -179,6 +105,77 @@ describe('createToken', () => { expect(result).toBe(true); }); + + it('should create a token with policy', async () => { + const withStep = withStepFactory(); + + const _inputs = { + ...inputs, + policies: ['marmalade-v2.guard-policy-v1'], + networkId: config.defaults.networkId, + host: config.host, + }; + + const tokenId = await createTokenId(_inputs); + + expect(tokenId).toBeDefined(); + expect(tokenId).toMatch(/^t:.{43}$/); + + const result = await createToken( + { + ..._inputs, + policyConfig: { + guarded: true, + updatableURI: true, + }, + guards: {}, + tokenId: tokenId as string, + }, + config, + ) + .on( + 'sign', + withStep((step, tx) => { + expect(step).toBe(1); + expect(tx.sigs).toHaveLength(1); + expect(tx.sigs[0].sig).toBeTruthy(); + }), + ) + .on( + 'preflight', + withStep((step, prResult) => { + expect(step).toBe(2); + if (prResult.result.status === 'failure') { + expect(prResult.result.status).toBe('success'); + } else { + expect(prResult.result.data).toBe(true); + } + }), + ) + .on( + 'submit', + withStep((step, trDesc) => { + expect(step).toBe(3); + expect(trDesc.networkId).toBe(NetworkIds.development); + expect(trDesc.chainId).toBe(chainId); + expect(trDesc.requestKey).toBeTruthy(); + }), + ) + .on( + 'listen', + withStep((step, sbResult) => { + expect(step).toBe(4); + if (sbResult.result.status === 'failure') { + expect(sbResult.result.status).toBe('success'); + } else { + expect(sbResult.result.data).toBe(true); + } + }), + ) + .execute(); + + expect(result).toBe(true); + }); }); describe('mintToken', () => { @@ -244,21 +241,13 @@ describe('mintToken', () => { expect(result).toBe(true); - const balance = await getTokenBalance( - { - accountName: sourceAccount.account, - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - config, - ).execute(); + const balance = await getTokenBalance({ + accountName: sourceAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); expect(balance).toBe(1); }); @@ -289,43 +278,65 @@ describe('mintToken', () => { describe('getTokenBalance', () => { it('should get a balance', async () => { - const result = await getTokenBalance( - { - accountName: sourceAccount.account, - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - config, - ).execute(); + const result = await getTokenBalance({ + accountName: sourceAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); expect(result).toBeGreaterThan(0); }); it('should throw an error if token does not exist', async () => { const nonExistingTokenId = 'non-existing-token'; - const task = getTokenBalance( - { - accountName: sourceAccount.account, - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: nonExistingTokenId, - }, - config, + const task = getTokenBalance({ + accountName: sourceAccount.account, + tokenId: nonExistingTokenId, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); + + await expect(() => Promise.resolve(task)).rejects.toThrowError( + new Error( + `read: row not found: ${nonExistingTokenId}:${sourceAccount.account}`, + ), ); + }); +}); - await expect(() => task.execute()).rejects.toThrowError( +describe('getAccountDetails', () => { + it('should get the account details', async () => { + const result = await getAccountDetails({ + chainId, + accountName: sourceAccount.account, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); + + expect(result).toStrictEqual({ + account: sourceAccount.account, + balance: 1, + guard: { + keys: [sourceAccount.publicKey], + pred: 'keys-all' as const, + }, + id: tokenId, + }); + }); + it('should throw an error if token does not exist', async () => { + const nonExistingTokenId = 'non-existing-token'; + const task = getAccountDetails({ + chainId, + accountName: sourceAccount.account, + tokenId: nonExistingTokenId, + networkId: config.defaults.networkId, + host: config.host, + }); + + await expect(() => Promise.resolve(task)).rejects.toThrowError( new Error( `read: row not found: ${nonExistingTokenId}:${sourceAccount.account}`, ), @@ -335,20 +346,12 @@ describe('getTokenBalance', () => { describe('getTokenInfo', () => { it('should get the info', async () => { - const result = await getTokenInfo( - { - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - config, - ).execute(); + const result = await getTokenInfo({ + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); expect(result).toStrictEqual({ supply: 1, @@ -360,22 +363,14 @@ describe('getTokenInfo', () => { }); it('should throw an error if token does not exist', async () => { const nonExistingTokenId = 'non-existing-token'; - const task = getTokenInfo( - { - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: nonExistingTokenId, - }, - config, - ); + const task = getTokenInfo({ + chainId, + tokenId: nonExistingTokenId, + networkId: config.defaults.networkId, + host: config.host, + }); - await expect(() => task.execute()).rejects.toThrowError( + await expect(() => Promise.resolve(task)).rejects.toThrowError( new Error(`with-read: row not found: ${nonExistingTokenId}`), ); }); @@ -383,41 +378,25 @@ describe('getTokenInfo', () => { describe('getTokenUri', () => { it('should get the uri', async () => { - const result = await getUri( - { - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - config, - ).execute(); + const result = await getUri({ + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); expect(result).toBe(inputs.uri); }); it('should throw an error if token does not exist', async () => { const nonExistingTokenId = 'non-existing-token'; - const task = getUri( - { - chainId, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: nonExistingTokenId, - }, - config, - ); + const task = getUri({ + chainId, + tokenId: nonExistingTokenId, + networkId: config.defaults.networkId, + host: config.host, + }); - await expect(() => task.execute()).rejects.toThrowError( + await expect(() => Promise.resolve(task)).rejects.toThrowError( new Error(`with-read: row not found: ${nonExistingTokenId}`), ); }); @@ -467,60 +446,6 @@ describe('updateUri', () => { }); }); -describe('getAccountDetails', () => { - it('should get the account details', async () => { - const result = await getAccountDetails( - { - chainId, - accountName: sourceAccount.account, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - config, - ).execute(); - - expect(result).toStrictEqual({ - account: sourceAccount.account, - balance: 1, - guard: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - id: tokenId, - }); - }); - it('should throw an error if token does not exist', async () => { - const nonExistingTokenId = 'non-existing-token'; - const task = getAccountDetails( - { - chainId, - accountName: sourceAccount.account, - guard: { - account: sourceAccount.account, - keyset: { - keys: [sourceAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: nonExistingTokenId, - }, - config, - ); - - await expect(() => task.execute()).rejects.toThrowError( - new Error( - `read: row not found: ${nonExistingTokenId}:${sourceAccount.account}`, - ), - ); - }); -}); - describe('transferCreateToken', () => { it('should transfer-create a token', async () => { const withStep = withStepFactory(); @@ -590,21 +515,13 @@ describe('transferCreateToken', () => { expect(result).toBe(true); - const balance = await getTokenBalance( - { - accountName: secondaryTargetAccount.account, - chainId: chainId, - guard: { - account: secondaryTargetAccount.account, - keyset: { - keys: [secondaryTargetAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - { ...config, sign: createSignWithKeypair([secondaryTargetAccount]) }, - ).execute(); + const balance = await getTokenBalance({ + accountName: secondaryTargetAccount.account, + chainId: chainId, + tokenId: tokenId as string, + networkId: config.defaults.networkId, + host: config.host, + }); expect(balance).toBe(1); }); @@ -709,21 +626,13 @@ describe('burnToken', () => { expect(result).toBe(true); - const balance = await getTokenBalance( - { - accountName: secondaryTargetAccount.account, - chainId, - guard: { - account: secondaryTargetAccount.account, - keyset: { - keys: [secondaryTargetAccount.publicKey], - pred: 'keys-all' as const, - }, - }, - tokenId: tokenId as string, - }, - burnConfig, - ).execute(); + const balance = await getTokenBalance({ + accountName: secondaryTargetAccount.account, + tokenId: tokenId as string, + chainId, + networkId: config.defaults.networkId, + host: config.host, + }); expect(balance).toBe(0); }); diff --git a/packages/libs/client-utils/src/integration-tests/support/helpers.ts b/packages/libs/client-utils/src/integration-tests/support/helpers.ts index 5cc4556e4f..34599e2d91 100644 --- a/packages/libs/client-utils/src/integration-tests/support/helpers.ts +++ b/packages/libs/client-utils/src/integration-tests/support/helpers.ts @@ -1,4 +1,18 @@ +import { createSignWithKeypair } from '@kadena/client'; +import { + addData, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { IGeneralCapability } from '@kadena/client/lib/interfaces/type-utilities'; +import type { ChainId, ICap, IPactInt } from '@kadena/types'; +import { expect } from 'vitest'; +import { dirtyReadClient, submitClient } from '../../core'; import type { Any } from '../../core/utils/types'; +import type { CommonProps } from '../../marmalade/config'; +import { sourceAccount } from '../test-data/accounts'; export const withStepFactory = () => { let step = 0; @@ -14,3 +28,153 @@ export const withStepFactory = () => { return cb(step, ...args); }; }; + +export const waitFor = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +export const getBlockTime = async (props?: { chainId?: ChainId }) => { + const { chainId } = props || { chainId: '0' }; + + const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), + }; + + const time = await dirtyReadClient(config)( + composePactCommand( + execution( + `(floor (diff-time (at 'block-time (chain-data)) (time "1970-01-01T00:00:00Z")))`, + ), + setMeta({ + senderAccount: sourceAccount.account, + chainId, + }), + ), + ).execute(); + + return new Date(Number(time) * 1000); +}; + +export const waitForBlockTime = async (timeMs: number) => { + while (true) { + const time = await getBlockTime(); + + if (time.getTime() >= timeMs) { + break; + } + + await waitFor(1000); + } +}; + +export const addDaysToDate = (originalDate: Date, daysToAdd: number) => + new Date(originalDate.getTime() + daysToAdd * 86400000); + +export const addMinutesToDate = (originalDate: Date, minutesToAdd: number) => + new Date(originalDate.getTime() + minutesToAdd * 60000); + +export const addSecondsToDate = (originalDate: Date, secondsToAdd: number) => + new Date(originalDate.getTime() + secondsToAdd * 1000); + +export const dateToPactInt = (date: Date): IPactInt => ({ + int: Math.floor(date.getTime() / 1000).toString(), +}); + +export const formatCapabilities = ( + capabilities: CommonProps['capabilities'] = [], + signFor: IGeneralCapability, +): ICap[] => + capabilities.map((capability) => + signFor(capability.name, ...capability.props), + ); + +export const formatAdditionalSigners = ( + additionalSigners: CommonProps['additionalSigners'] = [], +): any[] => + additionalSigners.map((signer) => + addSigner(signer.keyset.keys, (signFor) => + formatCapabilities(signer.capabilities, signFor), + ), + ); + +export const deployGasStation = async ({ chainId }: { chainId: ChainId }) => { + const config = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + }, + sign: createSignWithKeypair([sourceAccount]), + }; + + const result = await submitClient(config)( + composePactCommand( + execution( + ` + (namespace "free") + + (module test-gas-station GOVERNANCE + (defcap GOVERNANCE () true) + + (implements gas-payer-v1) + + (use coin) + + (defun chain-gas-price () + (at 'gas-price (chain-data)) + ) + + (defun enforce-below-or-at-gas-price:bool (gasPrice:decimal) + (enforce (<= (chain-gas-price) gasPrice) + (format "Gas Price must be smaller than or equal to {}" [gasPrice])) + ) + + (defcap GAS_PAYER:bool + ( user:string + limit:integer + price:decimal + ) + (enforce (= "exec" (at "tx-type" (read-msg))) "Can only be used inside an exec") + (enforce (= 1 (length (at "exec-code" (read-msg)))) "Can only be used to call one pact function") + (enforce-below-or-at-gas-price 0.000001) + (compose-capability (ALLOW_GAS)) + ) + + (defcap ALLOW_GAS () true) + + (defun create-gas-payer-guard:guard () + (create-user-guard (gas-payer-guard)) + ) + + (defun gas-payer-guard () + (require-capability (GAS)) + (require-capability (ALLOW_GAS)) + ) + + (defconst GAS_STATION_ACCOUNT "test-gas-station") + + (defun init () + (coin.create-account GAS_STATION_ACCOUNT (create-gas-payer-guard)) + ) + ) + + (if (read-msg "init") + [(init)] + ["not creating the gas station account"] + ) + `, + ), + addSigner(sourceAccount.publicKey, (signFor) => [signFor('coin.GAS')]), + addData('init', true), + setMeta({ + senderAccount: sourceAccount.account, + chainId, + gasLimit: 100_000, + }), + ), + ).execute(); + + expect(result).toStrictEqual(['Write succeeded']); +}; diff --git a/packages/libs/client-utils/src/marmalade/burn-token.ts b/packages/libs/client-utils/src/marmalade/burn-token.ts index ae62842db2..656f2f4909 100644 --- a/packages/libs/client-utils/src/marmalade/burn-token.ts +++ b/packages/libs/client-utils/src/marmalade/burn-token.ts @@ -13,8 +13,13 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { CommonProps } from './config'; -interface IBurnTokenInput { +interface IBurnTokenInput extends CommonProps { policyConfig?: { guarded?: boolean; nonFungible?: boolean; @@ -40,6 +45,9 @@ const burnTokenCommand = ({ guard, amount, policyConfig, + meta, + capabilities, + additionalSigners, }: IBurnTokenInput) => { if (policyConfig?.nonFungible) { throw new Error('Non-fungible tokens cannot be burned'); @@ -66,8 +74,10 @@ const burnTokenCommand = ({ ), ] : []), + ...formatCapabilities(capabilities, signFor), ]), - setMeta({ senderAccount: guard.account, chainId }), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: guard.account, chainId, ...meta }), ); }; diff --git a/packages/libs/client-utils/src/marmalade/buy-token.ts b/packages/libs/client-utils/src/marmalade/buy-token.ts new file mode 100644 index 0000000000..12247fb469 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/buy-token.ts @@ -0,0 +1,165 @@ +import type { + IPactModules, + IPartialPactCommand, + PactReturnType, +} from '@kadena/client'; +import { + addData, + addSigner, + composePactCommand, + continuation, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId, IPactDecimal, IPactInt } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { + CommonProps, + IAuctionPurchaseConfig, + IDutchAuctionPurchaseInput, + WithAuctionPurchase, +} from './config'; + +interface IBuyTokenInput extends CommonProps { + policyConfig?: { guarded: boolean }; + gasLimit?: number; + tokenId: string; + saleId: string; + amount: IPactDecimal; + timeout: IPactInt; + chainId: ChainId; + seller: { + account: string; + }; + buyer: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + buyerFungibleAccount?: string; +} + +const generatePolicyTransactionData = ( + props: Partial>, +): ((cmd: IPartialPactCommand) => IPartialPactCommand)[] => { + const data = []; + + data.push(addData('buyer', props.buyer!.account)); + data.push(addData('buyer-guard', props.buyer!.keyset)); + + if (props.buyerFungibleAccount) { + data.push(addData('buyer_fungible_account', props.buyerFungibleAccount)); + } + + if (props.auctionConfig?.dutch || props.auctionConfig?.conventional) { + data.push( + addData( + 'updated_price', + Number( + (props as unknown as IDutchAuctionPurchaseInput).updatedPrice.decimal, + ), + ), + ); + } + + if (props.auctionConfig?.dutch) { + data.push(addData('buyer_fungible_account', props.buyer!.account)); + } + + if (props.auctionConfig?.conventional) { + data.push( + addData( + 'buyer_fungible_account', + (props as unknown as IDutchAuctionPurchaseInput).escrow.account, + ), + ); + } + + return data; +}; + +const buyTokenCommand = ({ + auctionConfig, + tokenId, + saleId, + chainId, + seller, + buyer, + buyerFungibleAccount, + amount, + timeout, + gasLimit = 3000, + policyConfig, + meta, + capabilities, + additionalSigners, + ...props +}: WithAuctionPurchase) => + composePactCommand( + continuation({ + pactId: saleId, + step: 1, + rollback: false, + proof: null, + data: { + id: tokenId, + seller: seller.account, + amount, + timeout, + }, + }), + addSigner(buyer.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + 'marmalade-v2.ledger.BUY', + tokenId, + seller.account, + buyer.account, + amount, + saleId, + ), + ...(policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.SALE', + tokenId, + seller.account, + amount, + ), + ] + : []), + ...(auctionConfig?.dutch + ? [ + signFor( + 'coin.TRANSFER', + buyer.account, + (props as unknown as IDutchAuctionPurchaseInput).escrow.account, + (props as unknown as IDutchAuctionPurchaseInput).updatedPrice, + ), + ] + : []), + ...formatCapabilities(capabilities, signFor), + ]), + ...generatePolicyTransactionData({ + buyer, + buyerFungibleAccount, + auctionConfig, + ...props, + }), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: buyer.account, chainId, gasLimit, ...meta }), + ); + +export const buyToken = ( + inputs: WithAuctionPurchase, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(buyTokenCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/config.ts b/packages/libs/client-utils/src/marmalade/config.ts new file mode 100644 index 0000000000..bb4a96f56c --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/config.ts @@ -0,0 +1,331 @@ +import type { BuiltInPredicate } from '@kadena/client'; +import type { IPactDecimal, IPactInt } from '@kadena/types'; + +/** -----------COMMON----------- */ + +export interface CommonProps { + meta?: { + senderAccount?: string; + gasLimit?: number; + gasPrice?: number; + }; + capabilities?: { + name: string; + props: any[]; + }[]; + additionalSigners?: { + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + capabilities: { + name: string; + props: any[]; + }[]; + }[]; +} + +export interface KeysetGuard { + keys: string[]; + pred: BuiltInPredicate; +} + +export interface FunctionGuard { + args: string[]; + fun: string; +} + +export const GUARD_POLICY = 'marmalade-v2.guard-policy-v1'; +export const NON_FUNGIBLE_POLICY = 'marmalade-v2.non-fungible-policy-v1'; +export const ROYALTY_POLICY = 'marmalade-v2.royalty-policy-v1'; +export const COLLECTION_POLICY = 'marmalade-v2.collection-policy-v1'; + +export const GUARD_POLICY_SUCCESS_GUARD: FunctionGuard = { + args: [], + fun: `${GUARD_POLICY}.success`, +}; +export const GUARD_POLICY_FAILURE_GUARD: FunctionGuard = { + args: [], + fun: `${GUARD_POLICY}.failure`, +}; + +/** -----------COMMON----------- */ + +/** -----------CREATE----------- */ + +export interface IRoyaltyInfoInput { + fungible: { + refName: { + name: string; + namespace: string | null; + }; + refSpec: [ + { + name: string; + namespace: string | null; + }, + ]; + }; + creator: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; + royaltyRate: IPactDecimal; +} + +export interface IGuardInfoInput { + mintGuard?: KeysetGuard | FunctionGuard; + uriGuard?: KeysetGuard | FunctionGuard; + saleGuard?: KeysetGuard | FunctionGuard; + burnGuard?: KeysetGuard | FunctionGuard; + transferGuard?: KeysetGuard | FunctionGuard; +} + +export interface ICollectionInfoInput { + collectionId: string; +} + +export interface ICreateTokenPolicyConfig { + customPolicies?: boolean; + updatableURI?: boolean; + guarded?: boolean; + nonFungible?: boolean; + hasRoyalty?: boolean; + collection?: boolean; +} + +interface ConfigToDataMap { + customPolicies: { customPolicyData: Record }; + updatableURI: {}; + guarded: { guards: IGuardInfoInput }; + nonFungible: {}; + hasRoyalty: { royalty: IRoyaltyInfoInput }; + collection: { collection: ICollectionInfoInput }; +} + +export interface PolicyProps { + customPolicyData: Record; + guards: IGuardInfoInput; + royalty: IRoyaltyInfoInput; + collection: ICollectionInfoInput; +} + +type PolicyDataForConfig = + (C['customPolicies'] extends true ? ConfigToDataMap['customPolicies'] : {}) & + (C['updatableURI'] extends true ? ConfigToDataMap['updatableURI'] : {}) & + (C['guarded'] extends true ? ConfigToDataMap['guarded'] : {}) & + (C['nonFungible'] extends true ? ConfigToDataMap['nonFungible'] : {}) & + (C['hasRoyalty'] extends true ? ConfigToDataMap['hasRoyalty'] : {}) & + (C['collection'] extends true ? ConfigToDataMap['collection'] : {}); + +export type WithCreateTokenPolicy< + C extends ICreateTokenPolicyConfig, + Base, +> = Base & + (PolicyDataForConfig | undefined) & { + policyConfig?: C; + }; + +/** -----------CREATE----------- */ + +/** -----------SALE----------- */ + +export interface ISaleAuctionInfoInput { + fungible: { + refName: { + name: string; + namespace: string | null; + }; + refSpec: [ + { + name: string; + namespace: string | null; + }, + ]; + }; + price: IPactDecimal; + sellerFungibleAccount: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; + saleType?: + | 'marmalade-sale.conventional-auction' + | 'marmalade-sale.dutch-auction'; +} + +export interface ISaleGuardInfoInput { + saleGuard?: KeysetGuard | FunctionGuard; +} + +export interface ISaleTokenPolicyConfig { + auction?: boolean; + guarded?: boolean; + hasRoyalty?: boolean; +} + +export interface SalePolicyProps { + auction: ISaleAuctionInfoInput; + guards: ISaleGuardInfoInput; + royalty: IRoyaltyInfoInput; +} + +interface SaleConfigToDataMap { + auction: { auction: ISaleAuctionInfoInput }; + guarded: { guards: ISaleGuardInfoInput }; + hasRoyalty: { royalty: IRoyaltyInfoInput }; +} + +type PolicySaleDataForConfig = + (C['auction'] extends true ? SaleConfigToDataMap['auction'] : {}) & + (C['guarded'] extends true ? SaleConfigToDataMap['guarded'] : {}) & + (C['hasRoyalty'] extends true ? SaleConfigToDataMap['hasRoyalty'] : {}); + +export type WithSaleTokenPolicy = Base & + (PolicySaleDataForConfig | undefined) & { + policyConfig?: C; + }; + +/** -----------SALE----------- */ + +/** -----------WITHDRAW----------- */ + +export interface IWithdrawSaleGuardInfoInput { + saleGuard?: KeysetGuard | FunctionGuard; +} + +export interface IWithdrawSaleTokenPolicyConfig { + guarded?: boolean; +} + +export interface WithdrawSalePolicyProps { + guards: IWithdrawSaleGuardInfoInput; +} + +interface WithdrawSaleConfigToDataMap { + guarded: { guards: IWithdrawSaleGuardInfoInput }; +} + +type PolicyWithdrawSaleDataForConfig = + C['guarded'] extends true ? WithdrawSaleConfigToDataMap['guarded'] : {}; + +export type WithWithdrawSaleTokenPolicy< + C extends ICreateTokenPolicyConfig, + Base, +> = Base & + (PolicyWithdrawSaleDataForConfig | undefined) & { + policyConfig?: C; + }; + +/** -----------WITHDRAW----------- */ + +/** -----------AUCTION----------- */ + +export interface IConventionalAuctionInput { + saleId: string; + tokenId: string; + startDate: IPactInt; + endDate: IPactInt; + reservedPrice: IPactDecimal; +} + +export interface IDutchAuctionInput { + saleId: string; + tokenId: string; + startDate: IPactInt; + endDate: IPactInt; + startPrice: IPactDecimal; + reservedPrice: IPactDecimal; + priceIntervalInSeconds: IPactInt; +} + +export interface IAuctionConfig { + conventional?: boolean; + dutch?: boolean; +} + +export interface AuctionProps { + conventional: IConventionalAuctionInput; + dutch: IDutchAuctionInput; +} + +type AuctionDataForConfig = + (C['conventional'] extends true ? AuctionProps['conventional'] : {}) & + (C['dutch'] extends true ? AuctionProps['dutch'] : {}); + +export type WithAuction = Base & + (AuctionDataForConfig | undefined) & { + auctionConfig: C; + }; + +/** -----------AUCTION----------- */ + +/** -----------AUCTION-PURCHASE----------- */ + +export interface IConventionalAuctionPurchaseInput { + updatedPrice: IPactDecimal; + escrow: { + account: string; + }; +} + +export interface IDutchAuctionPurchaseInput { + updatedPrice: IPactDecimal; + escrow: { + account: string; + }; +} + +export interface IAuctionPurchaseConfig { + conventional?: boolean; + dutch?: boolean; +} + +export interface AuctionPurchaseProps { + conventional: IConventionalAuctionPurchaseInput; + dutch: IDutchAuctionPurchaseInput; +} + +type AuctionPurchaseDataForConfig = + C['conventional'] extends true + ? AuctionPurchaseProps['conventional'] + : {} & C['dutch'] extends true + ? AuctionPurchaseProps['dutch'] + : {}; + +export type WithAuctionPurchase = Base & + (AuctionPurchaseDataForConfig | undefined) & { + auctionConfig?: C; + }; + +/** -----------AUCTION-PURCHASE----------- */ + +/** -----------PLACE-BID----------- */ + +export interface IPlaceBidInput { + mkAccount: string; + mkFeePercentage: IPactDecimal; +} + +export interface IPlaceBidConfig { + marketplaceFee?: boolean; +} + +export interface PlaceBidProps { + marketplaceFee: IPlaceBidInput; +} + +type PlaceBidDataForConfig = + C['marketplaceFee'] extends true ? IPlaceBidInput : {}; + +export type WithPlaceBid = Base & + (PlaceBidDataForConfig | undefined) & { + marketplaceConfig?: C; + }; + +/** -----------PLACE-BID----------- */ diff --git a/packages/libs/client-utils/src/marmalade/create-auction.ts b/packages/libs/client-utils/src/marmalade/create-auction.ts new file mode 100644 index 0000000000..e234d7e6d1 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/create-auction.ts @@ -0,0 +1,126 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { + CommonProps, + IAuctionConfig, + IConventionalAuctionInput, + IDutchAuctionInput, + WithAuction, +} from './config'; + +interface ICreateAuctionInput extends CommonProps { + chainId: ChainId; + seller: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +const createConventionalAuctionCommand = ({ + chainId, + seller, + auctionConfig, + meta, + capabilities, + additionalSigners, + ...auctionProps +}: WithAuction) => { + const { saleId, tokenId, startDate, endDate, reservedPrice } = + auctionProps as unknown as IConventionalAuctionInput; + + return composePactCommand( + execution( + Pact.modules['marmalade-sale.conventional-auction']['create-auction']( + saleId, + tokenId, + startDate, + endDate, + reservedPrice, + ), + ), + addSigner(seller.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + `marmalade-sale.conventional-auction.MANAGE_AUCTION`, + saleId, + tokenId, + ), + ...formatCapabilities(capabilities, signFor), + ]), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: seller.account, chainId, ...meta }), + ); +}; + +const createDutchAuctionCommand = ({ + chainId, + seller, + auctionConfig, + ...auctionProps +}: WithAuction) => { + const { + saleId, + tokenId, + startDate, + endDate, + startPrice, + reservedPrice, + priceIntervalInSeconds, + } = auctionProps as unknown as IDutchAuctionInput; + + return composePactCommand( + execution( + Pact.modules['marmalade-sale.dutch-auction']['create-auction']( + saleId, + tokenId, + startDate, + endDate, + reservedPrice, + startPrice, + priceIntervalInSeconds, + ), + ), + addSigner(seller.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor(`marmalade-sale.dutch-auction.MANAGE_AUCTION`, saleId, tokenId), + ]), + setMeta({ senderAccount: seller.account, chainId }), + ); +}; + +export const createAuction = ( + inputs: WithAuction, + config: IClientConfig, +) => { + if (inputs.auctionConfig?.conventional) + return submitClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['create-auction'] + > + >(config)(createConventionalAuctionCommand(inputs)); + + if (inputs.auctionConfig?.dutch) + return submitClient< + PactReturnType< + IPactModules['marmalade-sale.dutch-auction']['create-auction'] + > + >(config)(createDutchAuctionCommand(inputs)); + + throw new Error('Invalid sale type'); +}; diff --git a/packages/libs/client-utils/src/marmalade/create-bid-id.ts b/packages/libs/client-utils/src/marmalade/create-bid-id.ts new file mode 100644 index 0000000000..c5ee9c2d58 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/create-bid-id.ts @@ -0,0 +1,42 @@ +import type { ChainId, IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface ICreateBidIdInput { + saleId: string; + bidderAccount: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const createBidId = ({ + saleId, + bidderAccount, + chainId, + networkId, + host, +}: ICreateBidIdInput) => + pipe( + () => + Pact.modules['marmalade-sale.conventional-auction']['create-bid-id']( + saleId, + bidderAccount, + ), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['create-bid-id'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/create-collection-id.ts b/packages/libs/client-utils/src/marmalade/create-collection-id.ts new file mode 100644 index 0000000000..10514439e6 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/create-collection-id.ts @@ -0,0 +1,53 @@ +import type { + BuiltInPredicate, + ChainId, + IPactModules, + PactReturnType, +} from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; +import { addKeyset, execution } from '@kadena/client/fp'; +import type { NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface ICreateCollectionIdInput { + collectionName: string; + operator: { + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const createCollectionId = ({ + collectionName, + operator, + chainId, + networkId, + host, +}: ICreateCollectionIdInput) => + pipe( + () => + Pact.modules['marmalade-v2.collection-policy-v1']['create-collection-id']( + collectionName, + readKeyset('operator-guard'), + ), + execution, + addKeyset('operator-guard', operator.keyset.pred, ...operator.keyset.keys), + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-v2.collection-policy-v1']['create-collection-id'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/create-collection.ts b/packages/libs/client-utils/src/marmalade/create-collection.ts new file mode 100644 index 0000000000..229a2c438f --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/create-collection.ts @@ -0,0 +1,75 @@ +import type { + BuiltInPredicate, + ChainId, + IPactModules, + PactReference, + PactReturnType, +} from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; +import { + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { IPactInt } from '@kadena/types'; +import { submitClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; +import type { CommonProps } from './config'; + +interface ICreateCollectionInput extends Pick { + id: string; + name: string; + size: IPactInt | PactReference; + chainId: ChainId; + operator: { + account: string; + keyset: { + keys: string[]; + pred: BuiltInPredicate; + }; + }; +} + +const createCollectionCommand = ({ + id, + name, + size, + operator, + chainId, + meta, +}: ICreateCollectionInput) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.collection-policy-v1']['create-collection']( + id, + name, + size, + readKeyset('operator-guard'), + ), + ), + setMeta({ senderAccount: operator.account, chainId }), + addKeyset('operator-guard', operator.keyset.pred, ...operator.keyset.keys), + addSigner(operator.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + 'marmalade-v2.collection-policy-v1.COLLECTION', + id, + name, + size, + operator.keyset, + ), + ]), + setMeta({ senderAccount: operator.account, chainId, ...meta }), + ); + +export const createCollection = ( + inputs: ICreateCollectionInput, + config: IClientConfig, +) => + submitClient< + PactReturnType< + IPactModules['marmalade-v2.collection-policy-v1']['create-collection'] + > + >(config)(createCollectionCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/create-token-id.ts b/packages/libs/client-utils/src/marmalade/create-token-id.ts index 4e2c451412..0905d56799 100644 --- a/packages/libs/client-utils/src/marmalade/create-token-id.ts +++ b/packages/libs/client-utils/src/marmalade/create-token-id.ts @@ -6,59 +6,63 @@ import type { PactReturnType, } from '@kadena/client'; import { Pact, readKeyset } from '@kadena/client'; -import { - addKeyset, - composePactCommand, - execution, - setMeta, -} from '@kadena/client/fp'; -import type { IPactInt } from '@kadena/types'; +import { addKeyset, execution } from '@kadena/client/fp'; +import type { IPactInt, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; -import type { ICreateTokenPolicyConfig } from './policy-config'; -import { validatePolicies } from './policy-config'; +import type { ICreateTokenPolicyConfig } from './config'; +import { validatePolicies } from './helpers'; interface ICreateTokenIdInput { policyConfig?: ICreateTokenPolicyConfig; policies?: string[]; uri: string; precision: IPactInt | PactReference; - chainId: ChainId; creator: { - account: string; keyset: { keys: string[]; pred: BuiltInPredicate; }; }; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; } -const createTokenIdCommand = ({ +export const createTokenId = ({ policies = [], uri, precision, creator, - chainId, policyConfig, + chainId, + networkId, + host, }: ICreateTokenIdInput) => { validatePolicies(policyConfig, policies); - return composePactCommand( - execution( + return pipe( + () => Pact.modules['marmalade-v2.ledger']['create-token-id']( - { precision, uri, policies }, + { + precision, + uri, + policies: () => + policies.length > 0 ? `[${policies.join(' ')}]` : '[]', + }, readKeyset('creation-guard'), ), - ), + execution, addKeyset('creation-guard', creator.keyset.pred, ...creator.keyset.keys), - setMeta({ senderAccount: creator.account, chainId }), - ); + dirtyReadClient< + PactReturnType + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); }; - -export const createTokenId = ( - inputs: ICreateTokenIdInput, - config: Omit, -) => - dirtyReadClient< - PactReturnType - >(config)(createTokenIdCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 56352e80cb..864943f7ff 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -16,18 +16,26 @@ import { execution, setMeta, } from '@kadena/client/fp'; +import type { ValidDataTypes } from '@kadena/client/lib/composePactCommand/utils/addData'; import type { IGeneralCapability } from '@kadena/client/lib/interfaces/type-utilities'; import type { IPactInt } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; import type { + CommonProps, + FunctionGuard, ICreateTokenPolicyConfig, PolicyProps, WithCreateTokenPolicy, -} from './policy-config'; -import { validatePolicies } from './policy-config'; +} from './config'; +import { GUARD_POLICY } from './config'; +import { validatePolicies } from './helpers'; -interface ICreateTokenInput { +interface ICreateTokenInput extends CommonProps { policies?: string[]; uri: string; tokenId: string; @@ -74,27 +82,75 @@ const generatePolicyTransactionData = ( if (policyConfig?.guarded) { if (props.guards.mintGuard) - data.push(addData('mint_guard', props.guards.mintGuard)); + data.push( + addData( + 'mint_guard', + props.guards.mintGuard as unknown as ValidDataTypes, + ), + ); if (props.guards.burnGuard) - data.push(addData('burn_guard', props.guards.burnGuard)); + data.push( + addData( + 'burn_guard', + props.guards.burnGuard as unknown as ValidDataTypes, + ), + ); if (props.guards.saleGuard) - data.push(addData('sale_guard', props.guards.saleGuard)); + data.push( + addData( + 'sale_guard', + props.guards.saleGuard as unknown as ValidDataTypes, + ), + ); if (props.guards.transferGuard) - data.push(addData('transfer_guard', props.guards.transferGuard)); + data.push( + addData( + 'transfer_guard', + props.guards.transferGuard as unknown as ValidDataTypes, + ), + ); if (props.guards.uriGuard) - data.push(addData('uri_guard', props.guards.uriGuard)); + data.push( + addData( + 'uri_guard', + props.guards.uriGuard as unknown as ValidDataTypes, + ), + ); } if (policyConfig?.hasRoyalty) { - data.push(addData('fungible', props.royalty.fungible)); - data.push(addData('creator', props.royalty.creator)); - data.push(addData('creator-guard', props.royalty.creatorGuard)); - data.push(addData('royalty-rate', props.royalty.royaltyRate.decimal)); + data.push( + addData('royalty_specs', { + fungible: props.royalty.fungible, + creator: props.royalty.creator, + 'creator-guard': props.royalty.creator.keyset, + 'royalty-rate': props.royalty.royaltyRate.decimal, + }), + ); } - if (policyConfig?.upgradeableURI && !policyConfig?.guarded) { + if (!policyConfig?.guarded && policyConfig?.updatableURI) { if (props.guards.uriGuard) - data.push(addData('uri_guard', props.guards.uriGuard)); + data.push( + addData( + 'uri_guard', + props.guards.uriGuard as unknown as ValidDataTypes, + ), + ); + } + + if (policyConfig?.guarded && !policyConfig?.updatableURI) { + if (!props.guards.uriGuard) { + throw new Error('Non-updatable tokens require "uriGuard"'); + } + if (!(props.guards.uriGuard as FunctionGuard)?.fun) { + throw new Error('Non-updatable tokens require function guard'); + } + if ( + (props.guards.uriGuard as FunctionGuard).fun !== `${GUARD_POLICY}.failure` + ) { + throw new Error('Non-updatable tokens require failure guard'); + } } if (policyConfig?.customPolicies) { @@ -114,6 +170,9 @@ const createTokenCommand = ({ creator, chainId, policyConfig, + meta, + capabilities, + additionalSigners, ...policyProps }: WithCreateTokenPolicy) => { validatePolicies(policyConfig as ICreateTokenPolicyConfig, policies); @@ -123,9 +182,7 @@ const createTokenCommand = ({ tokenId, precision, uri, - policies.length > 0 - ? ([policies.join(' ')] as unknown as PactReference) - : ([] as unknown as PactReference), + () => (policies.length > 0 ? `[${policies.join(' ')}]` : '[]'), readKeyset('creation-guard'), ), ), @@ -142,11 +199,14 @@ const createTokenCommand = ({ }, signFor, ), + ...formatCapabilities(capabilities, signFor), ]), ...generatePolicyTransactionData( policyConfig as ICreateTokenPolicyConfig, policyProps as unknown as PolicyProps, ), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: creator.account, chainId, ...meta }), ); }; diff --git a/packages/libs/client-utils/src/marmalade/escrow-account.ts b/packages/libs/client-utils/src/marmalade/escrow-account.ts new file mode 100644 index 0000000000..b103309afa --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/escrow-account.ts @@ -0,0 +1,39 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IEscrowAccountInput { + saleId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const escrowAccount = ({ + saleId, + chainId, + networkId, + host, +}: IEscrowAccountInput) => + pipe( + () => + Pact.modules['marmalade-sale.conventional-auction']['escrow-account']( + saleId, + ), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['escrow-account'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-account-details.ts b/packages/libs/client-utils/src/marmalade/get-account-details.ts index 1a20643da4..83aa573b6d 100644 --- a/packages/libs/client-utils/src/marmalade/get-account-details.ts +++ b/packages/libs/client-utils/src/marmalade/get-account-details.ts @@ -1,53 +1,39 @@ -import type { - BuiltInPredicate, - IPactModules, - PactReturnType, -} from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; import { Pact } from '@kadena/client'; -import { - addSigner, - composePactCommand, - execution, - setMeta, -} from '@kadena/client/fp'; -import type { ChainId } from '@kadena/types'; -import { submitClient } from '../core/client-helpers'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; interface IGetAccountBalanceInput { tokenId: string; accountName: string; chainId: ChainId; - guard: { - account: string; - keyset: { - keys: string[]; - pred: BuiltInPredicate; - }; - }; + networkId: NetworkId; + host?: IClientConfig['host']; } -const getAccountDetailsCommand = ({ +export const getAccountDetails = async ({ tokenId, accountName, chainId, - guard, -}: IGetAccountBalanceInput) => - composePactCommand( - execution( - Pact.modules['marmalade-v2.ledger'].details(tokenId, accountName), - ), - addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), - setMeta({ - senderAccount: guard.account, - chainId, + networkId, + host, +}: IGetAccountBalanceInput) => { + const result = await pipe( + () => Pact.modules['marmalade-v2.ledger'].details(tokenId, accountName), + execution, + dirtyReadClient< + PactReturnType + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, }), - ); + )().execute(); -export const getAccountDetails = ( - inputs: IGetAccountBalanceInput, - config: IClientConfig, -) => - submitClient>( - config, - )(getAccountDetailsCommand(inputs)); + return result; +}; diff --git a/packages/libs/client-utils/src/marmalade/get-auction-details.ts b/packages/libs/client-utils/src/marmalade/get-auction-details.ts new file mode 100644 index 0000000000..53f6f11089 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-auction-details.ts @@ -0,0 +1,62 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; +import type { IAuctionConfig } from './config'; + +interface IGetAuctionDetailsInput { + auctionConfig: IAuctionConfig; + saleId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getAuctionDetails = async ({ + auctionConfig, + saleId, + chainId, + host, + networkId, +}: IGetAuctionDetailsInput) => { + const config = { + host, + defaults: { + networkId, + meta: { chainId }, + }, + }; + + if (auctionConfig?.conventional) + return pipe( + () => + Pact.modules['marmalade-sale.conventional-auction']['retrieve-auction']( + saleId, + ), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['retrieve-auction'] + > + >(config), + )().execute(); + + if (auctionConfig?.dutch) + return pipe( + () => + Pact.modules['marmalade-sale.dutch-auction']['retrieve-auction']( + saleId, + ), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-sale.dutch-auction']['retrieve-auction'] + > + >(config), + )().execute(); + + throw new Error('Invalid sale type'); +}; diff --git a/packages/libs/client-utils/src/marmalade/get-bid.ts b/packages/libs/client-utils/src/marmalade/get-bid.ts new file mode 100644 index 0000000000..b1c19e618c --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-bid.ts @@ -0,0 +1,34 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetBidInput { + bidId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getBid = ({ bidId, chainId, networkId, host }: IGetBidInput) => + pipe( + () => + Pact.modules['marmalade-sale.conventional-auction']['retrieve-bid']( + bidId, + ), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['retrieve-bid'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-collection-token.ts b/packages/libs/client-utils/src/marmalade/get-collection-token.ts new file mode 100644 index 0000000000..a7868accfb --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-collection-token.ts @@ -0,0 +1,37 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetCollectionTokenInput { + tokenId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getCollectionToken = ({ + tokenId, + chainId, + networkId, + host, +}: IGetCollectionTokenInput) => + pipe( + () => + Pact.modules['marmalade-v2.collection-policy-v1']['get-token'](tokenId), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-v2.collection-policy-v1']['get-token'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-collection.ts b/packages/libs/client-utils/src/marmalade/get-collection.ts new file mode 100644 index 0000000000..0d9b7f4355 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-collection.ts @@ -0,0 +1,39 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetCollectionInput { + collectionId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getCollection = ({ + collectionId, + chainId, + networkId, + host, +}: IGetCollectionInput) => + pipe( + () => + Pact.modules['marmalade-v2.collection-policy-v1']['get-collection']( + collectionId, + ), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-v2.collection-policy-v1']['get-collection'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-current-price.ts b/packages/libs/client-utils/src/marmalade/get-current-price.ts new file mode 100644 index 0000000000..a34e5607da --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-current-price.ts @@ -0,0 +1,37 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetCurrentPriceInput { + saleId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getCurrentPrice = ({ + saleId, + chainId, + networkId, + host, +}: IGetCurrentPriceInput) => + pipe( + () => + Pact.modules['marmalade-sale.dutch-auction']['get-current-price'](saleId), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-sale.dutch-auction']['get-current-price'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-escrow-account.ts b/packages/libs/client-utils/src/marmalade/get-escrow-account.ts new file mode 100644 index 0000000000..1e34395bb6 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-escrow-account.ts @@ -0,0 +1,37 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetEscrowInput { + saleId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getEscrowAccount = ({ + saleId, + chainId, + networkId, + host, +}: IGetEscrowInput) => + pipe( + () => + Pact.modules['marmalade-v2.policy-manager']['get-escrow-account'](saleId), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-v2.policy-manager']['get-escrow-account'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-token-balance.ts b/packages/libs/client-utils/src/marmalade/get-token-balance.ts index 9eeb4dca9e..22ba0a4167 100644 --- a/packages/libs/client-utils/src/marmalade/get-token-balance.ts +++ b/packages/libs/client-utils/src/marmalade/get-token-balance.ts @@ -1,55 +1,37 @@ -import type { - BuiltInPredicate, - IPactModules, - PactReturnType, -} from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; import { Pact } from '@kadena/client'; -import { - addKeyset, - addSigner, - composePactCommand, - execution, - setMeta, -} from '@kadena/client/fp'; -import type { ChainId } from '@kadena/types'; -import { submitClient } from '../core/client-helpers'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; interface IGetBalanceInput { tokenId: string; - chainId: ChainId; accountName: string; - guard: { - account: string; - keyset: { - keys: string[]; - pred: BuiltInPredicate; - }; - }; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; } -const getTokenBalanceCommand = ({ +export const getTokenBalance = ({ tokenId, - chainId, accountName, - guard, + chainId, + networkId, + host, }: IGetBalanceInput) => - composePactCommand( - execution( + pipe( + () => Pact.modules['marmalade-v2.ledger']['get-balance'](tokenId, accountName), - ), - addKeyset('guard', 'keys-all', ...guard.keyset.keys), - addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), - setMeta({ - senderAccount: guard.account, - chainId, + execution, + dirtyReadClient< + PactReturnType + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, }), - ); - -export const getTokenBalance = ( - inputs: IGetBalanceInput, - config: IClientConfig, -) => - submitClient< - PactReturnType - >(config)(getTokenBalanceCommand(inputs)); + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-token-info.ts b/packages/libs/client-utils/src/marmalade/get-token-info.ts index 73f80feabf..391840b835 100644 --- a/packages/libs/client-utils/src/marmalade/get-token-info.ts +++ b/packages/libs/client-utils/src/marmalade/get-token-info.ts @@ -1,45 +1,34 @@ -import type { - BuiltInPredicate, - IPactModules, - PactReturnType, -} from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; import { Pact } from '@kadena/client'; -import { - addSigner, - composePactCommand, - execution, - setMeta, -} from '@kadena/client/fp'; -import type { ChainId } from '@kadena/types'; -import { submitClient } from '../core/client-helpers'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; interface IGetTokenInfoInput { tokenId: string; chainId: ChainId; - guard: { - account: string; - keyset: { - keys: string[]; - pred: BuiltInPredicate; - }; - }; + networkId: NetworkId; + host?: IClientConfig['host']; } -const getTokenInfoCommand = ({ tokenId, chainId, guard }: IGetTokenInfoInput) => - composePactCommand( - execution(Pact.modules['marmalade-v2.ledger']['get-token-info'](tokenId)), - addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), - setMeta({ - senderAccount: guard.account, - chainId, +export const getTokenInfo = ({ + tokenId, + chainId, + networkId, + host, +}: IGetTokenInfoInput) => + pipe( + () => Pact.modules['marmalade-v2.ledger']['get-token-info'](tokenId), + execution, + dirtyReadClient< + PactReturnType + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, }), - ); - -export const getTokenInfo = ( - inputs: IGetTokenInfoInput, - config: IClientConfig, -) => - submitClient< - PactReturnType - >(config)(getTokenInfoCommand(inputs)); + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/get-uri.ts b/packages/libs/client-utils/src/marmalade/get-uri.ts index e89a10ccc6..68a861e1c8 100644 --- a/packages/libs/client-utils/src/marmalade/get-uri.ts +++ b/packages/libs/client-utils/src/marmalade/get-uri.ts @@ -1,42 +1,29 @@ -import type { - BuiltInPredicate, - IPactModules, - PactReturnType, -} from '@kadena/client'; +import type { IPactModules, PactReturnType } from '@kadena/client'; import { Pact } from '@kadena/client'; -import { - addSigner, - composePactCommand, - execution, - setMeta, -} from '@kadena/client/fp'; -import type { ChainId } from '@kadena/types'; -import { submitClient } from '../core/client-helpers'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; interface IGetUriInput { tokenId: string; chainId: ChainId; - guard: { - account: string; - keyset: { - keys: string[]; - pred: BuiltInPredicate; - }; - }; + networkId: NetworkId; + host?: IClientConfig['host']; } -const getUriCommand = ({ tokenId, chainId, guard }: IGetUriInput) => - composePactCommand( - execution(Pact.modules['marmalade-v2.ledger']['get-uri'](tokenId)), - addSigner(guard.keyset.keys, (signFor) => [signFor('coin.GAS')]), - setMeta({ - senderAccount: guard.account, - chainId, +export const getUri = ({ tokenId, chainId, networkId, host }: IGetUriInput) => + pipe( + () => Pact.modules['marmalade-v2.ledger']['get-uri'](tokenId), + execution, + dirtyReadClient< + PactReturnType + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, }), - ); - -export const getUri = (inputs: IGetUriInput, config: IClientConfig) => - submitClient>( - config, - )(getUriCommand(inputs)); + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/helpers.ts b/packages/libs/client-utils/src/marmalade/helpers.ts new file mode 100644 index 0000000000..756815a0b1 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/helpers.ts @@ -0,0 +1,40 @@ +import type { ICreateTokenPolicyConfig } from './config'; +import { + COLLECTION_POLICY, + GUARD_POLICY, + NON_FUNGIBLE_POLICY, + ROYALTY_POLICY, +} from './config'; + +export const validatePolicies = ( + policyConfig?: ICreateTokenPolicyConfig, + policies: string[] = [], +) => { + if (policyConfig?.collection) { + if (!policies.includes(COLLECTION_POLICY)) { + throw new Error('Collection policy is required'); + } + } + + if (policyConfig?.guarded || policyConfig?.updatableURI) { + if (!policies.includes(GUARD_POLICY)) { + throw new Error('Guard policy is required'); + } + } + + if (policyConfig?.hasRoyalty) { + if (!policies.includes(ROYALTY_POLICY)) { + throw new Error('Royalty policy is required'); + } + } + + if (policyConfig?.nonFungible) { + if (!policies.includes(NON_FUNGIBLE_POLICY)) { + throw new Error('Non-fungible policy is required'); + } + } + + if (new Set(policies).size !== policies.length) { + throw new Error('Duplicate policies are not allowed'); + } +}; diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index fa2a7a1b03..108e019493 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -1,12 +1,27 @@ export * from './burn-token'; +export * from './buy-token'; +export * from './create-auction'; +export * from './create-bid-id'; +export * from './create-collection'; +export * from './create-collection-id'; export * from './create-token'; export * from './create-token-id'; +export * from './escrow-account'; export * from './get-account-details'; +export * from './get-auction-details'; +export * from './get-bid'; +export * from './get-collection'; +export * from './get-collection-token'; +export * from './get-current-price'; +export * from './get-escrow-account'; export * from './get-token-balance'; export * from './get-token-info'; export * from './get-uri'; export * from './mint-token'; -export * from './policy-config'; +export * from './offer-token'; +export * from './place-bid'; export * from './transfer-create-token'; export * from './transfer-token'; +export * from './update-auction'; export * from './update-uri'; +export * from './withdraw-token'; diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts index 5b9c19c14b..8575bd64ab 100644 --- a/packages/libs/client-utils/src/marmalade/mint-token.ts +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -10,8 +10,13 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { CommonProps } from './config'; -interface IMintTokenInput { +interface IMintTokenInput extends CommonProps { policyConfig?: { guarded?: boolean; nonFungible?: boolean; @@ -36,6 +41,9 @@ const mintTokenCommand = ({ guard, amount, policyConfig, + meta, + capabilities, + additionalSigners, }: IMintTokenInput) => { if (policyConfig?.nonFungible && amount.decimal !== '1') { throw new Error( @@ -66,8 +74,10 @@ const mintTokenCommand = ({ ), ] : []), + ...formatCapabilities(capabilities, signFor), ]), - setMeta({ senderAccount: guard.account, chainId }), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: guard.account, chainId, ...meta }), ); }; diff --git a/packages/libs/client-utils/src/marmalade/offer-token.ts b/packages/libs/client-utils/src/marmalade/offer-token.ts new file mode 100644 index 0000000000..e06a8b5ac7 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/offer-token.ts @@ -0,0 +1,121 @@ +import type { + IPactModules, + IPartialPactCommand, + PactReturnType, +} from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addData, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId, IPactDecimal, IPactInt } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { + CommonProps, + ISaleTokenPolicyConfig, + SalePolicyProps, + WithSaleTokenPolicy, +} from './config'; + +interface IOfferTokenInput extends CommonProps { + tokenId: string; + amount: IPactDecimal; + timeout: IPactInt; + chainId: ChainId; + seller: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +const generatePolicyTransactionData = ( + policyConfig: ISaleTokenPolicyConfig, + props: SalePolicyProps, +): ((cmd: IPartialPactCommand) => IPartialPactCommand)[] => { + const data = []; + + if (policyConfig?.auction) { + data.push( + addData('quote', { + fungible: props.auction.fungible, + 'sale-price': props.auction.price, + 'seller-fungible-account': { + account: props.auction.sellerFungibleAccount.account, + guard: props.auction.sellerFungibleAccount.keyset, + }, + 'sale-type': props.auction?.saleType ?? '', + }), + ); + } + + return data; +}; + +const offerTokenCommand = ({ + tokenId, + chainId, + seller, + amount, + timeout, + policyConfig, + meta, + capabilities, + additionalSigners, + ...policyProps +}: WithSaleTokenPolicy) => + composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger'].defpact.sale( + tokenId, + seller.account, + amount, + timeout, + ), + ), + addSigner(seller.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + 'marmalade-v2.ledger.OFFER', + tokenId, + seller.account, + amount, + timeout, + ), + ...(policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.SALE', + tokenId, + seller.account, + amount, + ), + ] + : []), + ...formatCapabilities(capabilities, signFor), + ]), + ...generatePolicyTransactionData( + policyConfig as ISaleTokenPolicyConfig, + policyProps as unknown as SalePolicyProps, + ), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: seller.account, chainId, ...meta }), + ); + +export const offerToken = ( + inputs: WithSaleTokenPolicy, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(offerTokenCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/place-bid.ts b/packages/libs/client-utils/src/marmalade/place-bid.ts new file mode 100644 index 0000000000..852bccd9b1 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/place-bid.ts @@ -0,0 +1,87 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact, readKeyset } from '@kadena/client'; +import { + addData, + addKeyset, + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId, IPactDecimal } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { + CommonProps, + IPlaceBidConfig, + PlaceBidProps, + WithPlaceBid, +} from './config'; + +interface IPlaceBidInput extends CommonProps { + saleId: string; + bid: IPactDecimal; + bidder: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; + escrowAccount: string; + chainId: ChainId; +} + +const placeBidCommand = ({ + saleId, + bid, + bidder, + escrowAccount, + chainId, + marketplaceConfig, + meta, + capabilities, + additionalSigners, + ...props +}: WithPlaceBid) => + composePactCommand( + execution( + Pact.modules['marmalade-sale.conventional-auction']['place-bid']( + saleId, + bidder.account, + readKeyset('account-guard'), + bid, + ), + ), + addKeyset('account-guard', bidder.keyset.pred, ...bidder.keyset.keys), + addSigner(bidder.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor('marmalade-sale.conventional-auction.PLACE_BID', bidder.keyset), + signFor('coin.TRANSFER', bidder.account, escrowAccount, bid), + ...formatCapabilities(capabilities, signFor), + ]), + marketplaceConfig?.marketplaceFee + ? addData('marketplace_fee', { + 'mk-account': (props as unknown as PlaceBidProps).marketplaceFee + .mkAccount, + 'mk-fee-percentage': (props as unknown as PlaceBidProps) + .marketplaceFee.mkFeePercentage, + }) + : {}, + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: bidder.account, chainId, ...meta }), + ); + +export const placeBid = ( + inputs: WithPlaceBid, + config: IClientConfig, +) => + submitClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['place-bid'] + > + >(config)(placeBidCommand(inputs)); diff --git a/packages/libs/client-utils/src/marmalade/policy-config.ts b/packages/libs/client-utils/src/marmalade/policy-config.ts index 03ad7f2dae..376d014769 100644 --- a/packages/libs/client-utils/src/marmalade/policy-config.ts +++ b/packages/libs/client-utils/src/marmalade/policy-config.ts @@ -1,5 +1,6 @@ import type { BuiltInPredicate } from '@kadena/client'; import type { IPactDecimal } from '@kadena/types'; + export interface IRoyaltyInfoInput { fungible: string; creator: string; @@ -39,7 +40,7 @@ export interface ICollectionInfoInput { export interface ICreateTokenPolicyConfig { customPolicies?: boolean; - upgradeableURI?: boolean; + updatableURI?: boolean; guarded?: boolean; nonFungible?: boolean; hasRoyalty?: boolean; @@ -53,7 +54,7 @@ export const COLLECTION_POLICY = 'collection-policy-v1'; interface ConfigToDataMap { customPolicies: { customPolicyData: Record }; - upgradeableURI: {}; + updatableURI: {}; guarded: { guards: IGuardInfoInput }; nonFungible: {}; hasRoyalty: { royalty: IRoyaltyInfoInput }; @@ -69,9 +70,7 @@ export interface PolicyProps { type PolicyDataForConfig = (C['customPolicies'] extends true ? ConfigToDataMap['customPolicies'] : {}) & - (C['upgradeableURI'] extends true - ? ConfigToDataMap['upgradeableURI'] - : {}) & + (C['updatableURI'] extends true ? ConfigToDataMap['updatableURI'] : {}) & (C['guarded'] extends true ? ConfigToDataMap['guarded'] : {}) & (C['nonFungible'] extends true ? ConfigToDataMap['nonFungible'] : {}) & (C['hasRoyalty'] extends true ? ConfigToDataMap['hasRoyalty'] : {}) & @@ -95,7 +94,7 @@ export const validatePolicies = ( } } - if (policyConfig?.guarded || policyConfig?.upgradeableURI) { + if (policyConfig?.guarded || policyConfig?.updatableURI) { if (!policies.includes(GUARD_POLICY)) { throw new Error('Guard policy is required'); } diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts index 004ab34a58..4f69f89826 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -10,8 +10,13 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { CommonProps } from './config'; -interface ITransferCreateTokenInput { +interface ITransferCreateTokenInput extends CommonProps { policyConfig?: { guarded?: boolean; hasRoyalty?: boolean; @@ -42,6 +47,9 @@ const createTransferTokenCommand = ({ receiver, amount, policyConfig, + meta, + capabilities, + additionalSigners, }: ITransferCreateTokenInput) => { if (policyConfig?.hasRoyalty) { throw new Error('Royalty tokens cannot be transferred.'); @@ -78,8 +86,10 @@ const createTransferTokenCommand = ({ ), ] : []), + ...formatCapabilities(capabilities, signFor), ]), - setMeta({ senderAccount: sender.account, chainId }), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: sender.account, chainId, ...meta }), ); }; diff --git a/packages/libs/client-utils/src/marmalade/transfer-token.ts b/packages/libs/client-utils/src/marmalade/transfer-token.ts index b85a325a78..0e60d43863 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-token.ts @@ -9,8 +9,13 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { CommonProps } from './config'; -interface ITransferTokenInput { +interface ITransferTokenInput extends CommonProps { policyConfig?: { guarded?: boolean; hasRoyalty?: boolean; @@ -37,6 +42,9 @@ const transferTokenCommand = ({ receiver, amount, policyConfig, + meta, + capabilities, + additionalSigners, }: ITransferTokenInput) => { if (policyConfig?.hasRoyalty) { throw new Error('Royalty tokens cannot be transferred.'); @@ -71,8 +79,10 @@ const transferTokenCommand = ({ ), ] : []), + ...formatCapabilities(capabilities, signFor), ]), - setMeta({ senderAccount: sender.account, chainId }), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: sender.account, chainId, ...meta }), ); }; diff --git a/packages/libs/client-utils/src/marmalade/update-auction.ts b/packages/libs/client-utils/src/marmalade/update-auction.ts new file mode 100644 index 0000000000..0c969703ee --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/update-auction.ts @@ -0,0 +1,124 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { + addSigner, + composePactCommand, + execution, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { + CommonProps, + IAuctionConfig, + IConventionalAuctionInput, + IDutchAuctionInput, + WithAuction, +} from './config'; + +interface IUpdateAuctionInput extends CommonProps { + chainId: ChainId; + seller: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +const updateConventionalAuctionCommand = ({ + chainId, + seller, + auctionConfig, + meta, + capabilities, + additionalSigners, + ...auctionProps +}: WithAuction) => { + const { saleId, tokenId, startDate, endDate, reservedPrice } = + auctionProps as unknown as IConventionalAuctionInput; + + return composePactCommand( + execution( + Pact.modules['marmalade-sale.conventional-auction']['update-auction']( + saleId, + startDate, + endDate, + reservedPrice, + ), + ), + addSigner(seller.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + `marmalade-sale.conventional-auction.MANAGE_AUCTION`, + saleId, + tokenId, + ), + ...formatCapabilities(capabilities, signFor), + ]), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: seller.account, chainId, ...meta }), + ); +}; + +const updateDutchAuctionCommand = ({ + chainId, + seller, + auctionConfig, + ...auctionProps +}: WithAuction) => { + const { + saleId, + tokenId, + startDate, + endDate, + startPrice, + reservedPrice, + priceIntervalInSeconds, + } = auctionProps as unknown as IDutchAuctionInput; + + return composePactCommand( + execution( + Pact.modules['marmalade-sale.dutch-auction']['update-auction']( + saleId, + startDate, + endDate, + startPrice, + reservedPrice, + priceIntervalInSeconds, + ), + ), + addSigner(seller.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor(`marmalade-sale.dutch-auction.MANAGE_AUCTION`, saleId, tokenId), + ]), + setMeta({ senderAccount: seller.account, chainId }), + ); +}; + +export const updateAuction = ( + inputs: WithAuction, + config: IClientConfig, +) => { + if (inputs.auctionConfig?.conventional) + return submitClient< + PactReturnType< + IPactModules['marmalade-sale.conventional-auction']['update-auction'] + > + >(config)(updateConventionalAuctionCommand(inputs)); + + if (inputs.auctionConfig?.dutch) + return submitClient< + PactReturnType< + IPactModules['marmalade-sale.dutch-auction']['update-auction'] + > + >(config)(updateDutchAuctionCommand(inputs)); + + throw new Error('Invalid sale type'); +}; diff --git a/packages/libs/client-utils/src/marmalade/withdraw-token.ts b/packages/libs/client-utils/src/marmalade/withdraw-token.ts new file mode 100644 index 0000000000..0667f3629d --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/withdraw-token.ts @@ -0,0 +1,93 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { + addSigner, + composePactCommand, + continuation, + setMeta, +} from '@kadena/client/fp'; +import type { ChainId, IPactDecimal, IPactInt } from '@kadena/types'; +import { submitClient } from '../core'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + formatAdditionalSigners, + formatCapabilities, +} from '../integration-tests/support/helpers'; +import type { + CommonProps, + IWithdrawSaleTokenPolicyConfig, + WithWithdrawSaleTokenPolicy, +} from './config'; + +interface IWithdrawTokenInput extends CommonProps { + tokenId: string; + saleId: string; + amount: IPactDecimal; + timeout: IPactInt; + chainId: ChainId; + seller: { + account: string; + keyset: { + keys: string[]; + pred: 'keys-all' | 'keys-2' | 'keys-any'; + }; + }; +} + +const withdrawTokenCommand = ({ + tokenId, + saleId, + chainId, + seller, + amount, + timeout, + policyConfig, + meta, + capabilities, + additionalSigners, +}: WithWithdrawSaleTokenPolicy) => + composePactCommand( + continuation({ + pactId: saleId, + step: 0, + rollback: true, + proof: null, + data: { + id: tokenId, + seller: seller.account, + amount, + timeout, + }, + }), + addSigner(seller.keyset.keys, (signFor) => [ + signFor('coin.GAS'), + signFor( + 'marmalade-v2.ledger.WITHDRAW', + tokenId, + seller.account, + amount, + timeout, + saleId, + ), + ...(policyConfig?.guarded + ? [ + signFor( + 'marmalade-v2.guard-policy-v1.SALE', + tokenId, + seller.account, + amount, + ), + ] + : []), + ...formatCapabilities(capabilities, signFor), + ]), + ...formatAdditionalSigners(additionalSigners), + setMeta({ senderAccount: seller.account, chainId, ...meta }), + ); + +export const withdrawToken = ( + inputs: WithWithdrawSaleTokenPolicy, + config: IClientConfig, +) => + submitClient< + PactReturnType + >(config)(withdrawTokenCommand(inputs)); diff --git a/packages/libs/client-utils/src/scripts/setup-marmalade-test-env.ts b/packages/libs/client-utils/src/scripts/setup-marmalade-test-env.ts new file mode 100644 index 0000000000..b09ac56aca --- /dev/null +++ b/packages/libs/client-utils/src/scripts/setup-marmalade-test-env.ts @@ -0,0 +1,93 @@ +import { createSignWithKeypair } from '@kadena/client'; +import type { ChainId } from '@kadena/types'; +import { describeModule } from '../built-in'; +import { sourceAccount } from '../built-in/test/test-data'; +import { transferCreate } from '../coin'; +import type { IClientConfig } from '../core/utils/helpers'; +import { + secondaryTargetAccount, + sender00Account, +} from '../integration-tests/test-data/accounts'; +import { deployMarmalade } from '../nodejs'; + +const chainId: ChainId = '0'; + +const main = async () => { + console.log('Setting up marmalade test environment'); + + const config: IClientConfig = { + host: 'http://127.0.0.1:8080', + defaults: { + networkId: 'development', + meta: { + chainId, + }, + }, + sign: createSignWithKeypair([sender00Account]), + }; + let marmaladeDeployed = false; + + try { + await describeModule('marmalade-v2.ledger', config); + marmaladeDeployed = true; + } catch (error) { + console.log('Marmalade not deployed, deploying now'); + } + + if (!marmaladeDeployed) { + await deployMarmalade({ + chainIds: [chainId], + deleteFilesAfterDeployment: true, + }); + } + + const [resultSourceAccount, resultTargetAccount] = await Promise.all([ + transferCreate( + { + sender: { + account: sender00Account.account, + publicKeys: [sender00Account.publicKey], + }, + receiver: { + account: sourceAccount.account, + keyset: { + keys: [sourceAccount.publicKey], + pred: 'keys-all', + }, + }, + amount: '100', + chainId, + }, + config, + ).execute(), + transferCreate( + { + sender: { + account: sender00Account.account, + publicKeys: [sender00Account.publicKey], + }, + receiver: { + account: secondaryTargetAccount.account, + keyset: { + keys: [secondaryTargetAccount.publicKey], + pred: 'keys-all', + }, + }, + amount: '100', + chainId, + }, + config, + ).execute(), + ]); + + if ( + resultSourceAccount !== 'Write succeeded' || + resultTargetAccount !== 'Write succeeded' + ) { + throw new Error('Toping up source and target accounts failed'); + } + + console.log('Marmalade test environment setup complete'); +}; + +main().catch(console.error); diff --git a/packages/libs/client-utils/vitest.integration.config.ts b/packages/libs/client-utils/vitest.integration.config.ts index a1d33a433f..f50be2ea72 100644 --- a/packages/libs/client-utils/vitest.integration.config.ts +++ b/packages/libs/client-utils/vitest.integration.config.ts @@ -3,6 +3,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { include: ['src/**/*.int.test.ts'], - testTimeout: 60000, + testTimeout: 150000, }, }); From 26ec4e3f9339353f8a137c5950979ebb6217e9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Maga=C5=A1?= Date: Mon, 27 May 2024 11:17:20 +0200 Subject: [PATCH 28/28] chore: get quote function and reorganize imports (#2187) --- .../src/integration-tests/support/helpers.ts | 21 +---------- .../client-utils/src/marmalade/burn-token.ts | 5 +-- .../client-utils/src/marmalade/buy-token.ts | 5 +-- .../src/marmalade/create-auction.ts | 5 +-- .../src/marmalade/create-token.ts | 10 +++--- .../src/marmalade/get-quote-info.ts | 36 +++++++++++++++++++ .../client-utils/src/marmalade/helpers.ts | 22 +++++++++++- .../libs/client-utils/src/marmalade/index.ts | 1 + .../client-utils/src/marmalade/mint-token.ts | 5 +-- .../client-utils/src/marmalade/offer-token.ts | 5 +-- .../client-utils/src/marmalade/place-bid.ts | 5 +-- .../src/marmalade/transfer-create-token.ts | 5 +-- .../src/marmalade/transfer-token.ts | 5 +-- .../src/marmalade/update-auction.ts | 5 +-- .../src/marmalade/withdraw-token.ts | 5 +-- 15 files changed, 74 insertions(+), 66 deletions(-) create mode 100644 packages/libs/client-utils/src/marmalade/get-quote-info.ts diff --git a/packages/libs/client-utils/src/integration-tests/support/helpers.ts b/packages/libs/client-utils/src/integration-tests/support/helpers.ts index 34599e2d91..c94545b392 100644 --- a/packages/libs/client-utils/src/integration-tests/support/helpers.ts +++ b/packages/libs/client-utils/src/integration-tests/support/helpers.ts @@ -6,12 +6,10 @@ import { execution, setMeta, } from '@kadena/client/fp'; -import type { IGeneralCapability } from '@kadena/client/lib/interfaces/type-utilities'; -import type { ChainId, ICap, IPactInt } from '@kadena/types'; +import type { ChainId, IPactInt } from '@kadena/types'; import { expect } from 'vitest'; import { dirtyReadClient, submitClient } from '../../core'; import type { Any } from '../../core/utils/types'; -import type { CommonProps } from '../../marmalade/config'; import { sourceAccount } from '../test-data/accounts'; export const withStepFactory = () => { @@ -83,23 +81,6 @@ export const dateToPactInt = (date: Date): IPactInt => ({ int: Math.floor(date.getTime() / 1000).toString(), }); -export const formatCapabilities = ( - capabilities: CommonProps['capabilities'] = [], - signFor: IGeneralCapability, -): ICap[] => - capabilities.map((capability) => - signFor(capability.name, ...capability.props), - ); - -export const formatAdditionalSigners = ( - additionalSigners: CommonProps['additionalSigners'] = [], -): any[] => - additionalSigners.map((signer) => - addSigner(signer.keyset.keys, (signFor) => - formatCapabilities(signer.capabilities, signFor), - ), - ); - export const deployGasStation = async ({ chainId }: { chainId: ChainId }) => { const config = { host: 'http://127.0.0.1:8080', diff --git a/packages/libs/client-utils/src/marmalade/burn-token.ts b/packages/libs/client-utils/src/marmalade/burn-token.ts index 656f2f4909..78a0e05221 100644 --- a/packages/libs/client-utils/src/marmalade/burn-token.ts +++ b/packages/libs/client-utils/src/marmalade/burn-token.ts @@ -13,11 +13,8 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IBurnTokenInput extends CommonProps { policyConfig?: { diff --git a/packages/libs/client-utils/src/marmalade/buy-token.ts b/packages/libs/client-utils/src/marmalade/buy-token.ts index 12247fb469..46129a3cf5 100644 --- a/packages/libs/client-utils/src/marmalade/buy-token.ts +++ b/packages/libs/client-utils/src/marmalade/buy-token.ts @@ -13,16 +13,13 @@ import { import type { ChainId, IPactDecimal, IPactInt } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, IAuctionPurchaseConfig, IDutchAuctionPurchaseInput, WithAuctionPurchase, } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IBuyTokenInput extends CommonProps { policyConfig?: { guarded: boolean }; diff --git a/packages/libs/client-utils/src/marmalade/create-auction.ts b/packages/libs/client-utils/src/marmalade/create-auction.ts index e234d7e6d1..e27e586f1e 100644 --- a/packages/libs/client-utils/src/marmalade/create-auction.ts +++ b/packages/libs/client-utils/src/marmalade/create-auction.ts @@ -9,10 +9,6 @@ import { import type { ChainId } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, IAuctionConfig, @@ -20,6 +16,7 @@ import type { IDutchAuctionInput, WithAuction, } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface ICreateAuctionInput extends CommonProps { chainId: ChainId; diff --git a/packages/libs/client-utils/src/marmalade/create-token.ts b/packages/libs/client-utils/src/marmalade/create-token.ts index 864943f7ff..ef127272b4 100644 --- a/packages/libs/client-utils/src/marmalade/create-token.ts +++ b/packages/libs/client-utils/src/marmalade/create-token.ts @@ -21,10 +21,6 @@ import type { IGeneralCapability } from '@kadena/client/lib/interfaces/type-util import type { IPactInt } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, FunctionGuard, @@ -33,7 +29,11 @@ import type { WithCreateTokenPolicy, } from './config'; import { GUARD_POLICY } from './config'; -import { validatePolicies } from './helpers'; +import { + formatAdditionalSigners, + formatCapabilities, + validatePolicies, +} from './helpers'; interface ICreateTokenInput extends CommonProps { policies?: string[]; diff --git a/packages/libs/client-utils/src/marmalade/get-quote-info.ts b/packages/libs/client-utils/src/marmalade/get-quote-info.ts new file mode 100644 index 0000000000..80f7b10be3 --- /dev/null +++ b/packages/libs/client-utils/src/marmalade/get-quote-info.ts @@ -0,0 +1,36 @@ +import type { IPactModules, PactReturnType } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { execution } from '@kadena/client/fp'; +import type { ChainId, NetworkId } from '@kadena/types'; +import { pipe } from 'ramda'; +import { dirtyReadClient } from '../core/client-helpers'; +import type { IClientConfig } from '../core/utils/helpers'; + +interface IGetAuctionDetailsInput { + saleId: string; + chainId: ChainId; + networkId: NetworkId; + host?: IClientConfig['host']; +} + +export const getQuoteInfo = async ({ + saleId, + chainId, + host, + networkId, +}: IGetAuctionDetailsInput) => + pipe( + () => Pact.modules['marmalade-v2.policy-manager']['get-quote-info'](saleId), + execution, + dirtyReadClient< + PactReturnType< + IPactModules['marmalade-v2.policy-manager']['get-quote-info'] + > + >({ + host, + defaults: { + networkId, + meta: { chainId }, + }, + }), + )().execute(); diff --git a/packages/libs/client-utils/src/marmalade/helpers.ts b/packages/libs/client-utils/src/marmalade/helpers.ts index 756815a0b1..1a183c2976 100644 --- a/packages/libs/client-utils/src/marmalade/helpers.ts +++ b/packages/libs/client-utils/src/marmalade/helpers.ts @@ -1,4 +1,7 @@ -import type { ICreateTokenPolicyConfig } from './config'; +import { addSigner } from '@kadena/client/fp'; +import type { IGeneralCapability } from '@kadena/client/lib/interfaces/type-utilities'; +import type { ICap } from '@kadena/types'; +import type { CommonProps, ICreateTokenPolicyConfig } from './config'; import { COLLECTION_POLICY, GUARD_POLICY, @@ -38,3 +41,20 @@ export const validatePolicies = ( throw new Error('Duplicate policies are not allowed'); } }; + +export const formatCapabilities = ( + capabilities: CommonProps['capabilities'] = [], + signFor: IGeneralCapability, +): ICap[] => + capabilities.map((capability) => + signFor(capability.name, ...capability.props), + ); + +export const formatAdditionalSigners = ( + additionalSigners: CommonProps['additionalSigners'] = [], +): any[] => + additionalSigners.map((signer) => + addSigner(signer.keyset.keys, (signFor) => + formatCapabilities(signer.capabilities, signFor), + ), + ); diff --git a/packages/libs/client-utils/src/marmalade/index.ts b/packages/libs/client-utils/src/marmalade/index.ts index 108e019493..3d9f54943a 100644 --- a/packages/libs/client-utils/src/marmalade/index.ts +++ b/packages/libs/client-utils/src/marmalade/index.ts @@ -14,6 +14,7 @@ export * from './get-collection'; export * from './get-collection-token'; export * from './get-current-price'; export * from './get-escrow-account'; +export * from './get-quote-info'; export * from './get-token-balance'; export * from './get-token-info'; export * from './get-uri'; diff --git a/packages/libs/client-utils/src/marmalade/mint-token.ts b/packages/libs/client-utils/src/marmalade/mint-token.ts index 8575bd64ab..a55d7f59af 100644 --- a/packages/libs/client-utils/src/marmalade/mint-token.ts +++ b/packages/libs/client-utils/src/marmalade/mint-token.ts @@ -10,11 +10,8 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IMintTokenInput extends CommonProps { policyConfig?: { diff --git a/packages/libs/client-utils/src/marmalade/offer-token.ts b/packages/libs/client-utils/src/marmalade/offer-token.ts index e06a8b5ac7..4a801cd88d 100644 --- a/packages/libs/client-utils/src/marmalade/offer-token.ts +++ b/packages/libs/client-utils/src/marmalade/offer-token.ts @@ -14,16 +14,13 @@ import { import type { ChainId, IPactDecimal, IPactInt } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, ISaleTokenPolicyConfig, SalePolicyProps, WithSaleTokenPolicy, } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IOfferTokenInput extends CommonProps { tokenId: string; diff --git a/packages/libs/client-utils/src/marmalade/place-bid.ts b/packages/libs/client-utils/src/marmalade/place-bid.ts index 852bccd9b1..a825c5eb62 100644 --- a/packages/libs/client-utils/src/marmalade/place-bid.ts +++ b/packages/libs/client-utils/src/marmalade/place-bid.ts @@ -11,16 +11,13 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, IPlaceBidConfig, PlaceBidProps, WithPlaceBid, } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IPlaceBidInput extends CommonProps { saleId: string; diff --git a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts index 4f69f89826..4c05342221 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-create-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-create-token.ts @@ -10,11 +10,8 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface ITransferCreateTokenInput extends CommonProps { policyConfig?: { diff --git a/packages/libs/client-utils/src/marmalade/transfer-token.ts b/packages/libs/client-utils/src/marmalade/transfer-token.ts index 0e60d43863..9a21aa62ce 100644 --- a/packages/libs/client-utils/src/marmalade/transfer-token.ts +++ b/packages/libs/client-utils/src/marmalade/transfer-token.ts @@ -9,11 +9,8 @@ import { import type { ChainId, IPactDecimal } from '@kadena/types'; import { submitClient } from '../core/client-helpers'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface ITransferTokenInput extends CommonProps { policyConfig?: { diff --git a/packages/libs/client-utils/src/marmalade/update-auction.ts b/packages/libs/client-utils/src/marmalade/update-auction.ts index 0c969703ee..20cb7a410f 100644 --- a/packages/libs/client-utils/src/marmalade/update-auction.ts +++ b/packages/libs/client-utils/src/marmalade/update-auction.ts @@ -9,10 +9,6 @@ import { import type { ChainId } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, IAuctionConfig, @@ -20,6 +16,7 @@ import type { IDutchAuctionInput, WithAuction, } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IUpdateAuctionInput extends CommonProps { chainId: ChainId; diff --git a/packages/libs/client-utils/src/marmalade/withdraw-token.ts b/packages/libs/client-utils/src/marmalade/withdraw-token.ts index 0667f3629d..e9d46c9ffe 100644 --- a/packages/libs/client-utils/src/marmalade/withdraw-token.ts +++ b/packages/libs/client-utils/src/marmalade/withdraw-token.ts @@ -8,15 +8,12 @@ import { import type { ChainId, IPactDecimal, IPactInt } from '@kadena/types'; import { submitClient } from '../core'; import type { IClientConfig } from '../core/utils/helpers'; -import { - formatAdditionalSigners, - formatCapabilities, -} from '../integration-tests/support/helpers'; import type { CommonProps, IWithdrawSaleTokenPolicyConfig, WithWithdrawSaleTokenPolicy, } from './config'; +import { formatAdditionalSigners, formatCapabilities } from './helpers'; interface IWithdrawTokenInput extends CommonProps { tokenId: string;