diff --git a/README.md b/README.md index 6770b351..f7e9e3d2 100644 --- a/README.md +++ b/README.md @@ -8,40 +8,65 @@ The ainft-js is typescript SDK to interact with AIN blockchain and create and manage AINFT. ## AINFT Factory -The AINFT Factory is a component consisting of AINFT Factory server and ainft-js. AINFT Factory supports the following two features: + +The AINFT Factory is a component consisting of AINFT Factory server and ainft-js. AINFT Factory supports the following two features: + - AINFT: Supports creating and managing AINFT, the NFT of the Ain blockchain. - Tokenomics: Supports functions for activating tokenomics in NFT communities. You can see reference about AINFT Factory: https://docs.ainetwork.ai/ainfts/ainft. -## Getting start +## Installation ```bash -npm install @ainft-team/ainft-js +yarn add @ainft-team/ainft-js ``` -After installing the app, you can then import and use the SDK -```javascript -const AinftJs = require('@ainft-team/ainft-js').default; +## Usage + +First, install the application and import the SDK to get started: -// Enter the private key for the account you want to use to create and manage AINFT. -// If you don't have an account, you can create it through ain wallet, -// or you can leverage tools/create_account.js. -const ainftJs = new AinftJs(); +```js +import AinftJs from '@ainft-team/ainft-js'; ``` -If you want to connect to the testnet of the Ain blockchain, you can set the Ain blockchain endpoint. -```javascript -const config = { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai' -} -const ainftJs = new AinftJs(, config); +### Configuration + +To initialize the SDK with a private key, create an instance of **\`AinftJs\`** as follows. If you don't have an account, create one through the AIN wallet or by using script at **\`examples/wallet/createAccount.js\`**; + +```js +const ainft = new AinftJs({ + privateKey: '', +}); +``` + +Alternatively, you can use the AIN wallet for authentication and transaction signing in the Chrome browser. + +```js +const ainft = new AinftJs({ + signer: new AinWalletSigner(), +}); +``` + +### Connecting to the Testnet + +To connect to the AIN blockchain testnet, configure the SDK with the testnet endpoints: + +```js +const ainft = new AinftJs({ + privateKey: '', + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); ``` ## Features + ### AINFT + You can create AINFT object and mint AINFT though AINFT object. Below modules support it. + - `nft`: Creates AINFT object and Searches AINFTs and AINFT objects. - `ainft721Object`: It is AINFT object class. Mints AINFTs and Transfers it to other accounts. - `ainftToken`: It is AINFT class. Updates metadata. @@ -49,13 +74,15 @@ You can create AINFT object and mint AINFT though AINFT object. Below modules su You can learn how to make AINFT in [tutorials](https://docs.ainetwork.ai/ainfts/developer-reference/ainft-tutorial). ### Tokenomics + Features for activating tokenomics in NFT communities. + - `credit`: Create and manage community-specific credits. - `event`: Create and manage events where user can take action and receive rewards. This is a function for credit mining. - `store`: You can create items, register them in the store, and sell them. This is a function for consuming credit. - ## NFT API + Introducing the main API functions that can be used in the `nft` module. - `create(name, symbol)`: Creates AINFT object. @@ -67,6 +94,7 @@ Introducing the main API functions that can be used in the `nft` module. - `searchAinfts(searchParams)`: Search for AINFT. You can use ainft object id, name, symbol, token id, user address for searching. ## AINFT721 Object API + Introducing the main API functions that can be used in the `ainftObject` module. - `getToken(tokenId)`: Gets AINFT that was minted by AINFT object. @@ -74,15 +102,46 @@ Introducing the main API functions that can be used in the `ainftObject` module. - `mint(to, tokenId)`: Mints AINFT. ## AINFT Token API + Introducing the main API functions that can be used in the `ainftToken` module. - `setMetadata(metadata)`: Sets metadata of AINFT. +## AI API +We provide AI API functions, including assistant, thread, and message. +To use these functions follow streamlined steps: +1. **Initialization**: Before using any AI functions, initialize the event channel with the `connect()` method. +2. **Using AI functions**: After opening the event channel, you can use AI function. +3. **Closure**: Ensure to close the event channel with the `disconnect()` method when the functions are no longer needed. + +```js +import AinftJs from '@ainft-team/ainft-js'; + +const ainft = new AinftJs({ + privateKey: '', + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); + +async function main() { + await ainft.connect(); // connect to the blockchain endpoint + + // your ai function usage here + // ... + + await ainft.disconnect(); // disconnect from the blockchain endpoint +} + +main(); +``` ## AINFT tutorial + You can view the [tutorial document](https://docs.ainetwork.ai/ainfts/developer-reference/ainft-tutorial) at the following link. and You can also look at scripts created for tutorials in the [tutorial directory](https://github.com/ainft-team/ainft-js/tree/main/examples). Tutorial scripts + - [createAinftObject](https://github.com/ainft-team/ainft-js/blob/master/examples/nft/createAinftObject.js) - [mintAinft](https://github.com/ainft-team/ainft-js/blob/master/examples/nft/mintNft.js) - [transferAinft](https://github.com/ainft-team/ainft-js/blob/master/examples/nft/transferNft.js) @@ -90,7 +149,9 @@ Tutorial scripts - [searchAinft](https://github.com/ainft-team/ainft-js/blob/master/examples/nft/search.js) ## API Documentation + API documentation is available at https://ainft-team.github.io/ainft-js. ## License -MIT License \ No newline at end of file + +MIT License diff --git a/examples/ai/assistant.js b/examples/ai/assistant.js new file mode 100644 index 00000000..e37ce3bf --- /dev/null +++ b/examples/ai/assistant.js @@ -0,0 +1,40 @@ +// To run this example, you must own an ainft object and token; create one if you don't. +// https://docs.ainetwork.ai/ainfts/developer-reference/ainft-tutorial/create-ainft-object-and-mint + +const AinftJs = require('@ainft-team/ainft-js').default; +const { privateKey, objectId, appId, tokenId } = require('../config.json'); // TODO(user): set these in config.json + +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); + +async function main() { + try { + console.log('Creating assistant...\n'); + + await ainft.connect(); + + const { assistant, tx_hash } = await ainft.assistant.create(objectId, tokenId, { + model: 'gpt-4', // TODO(user): update this + name: 'QuickSupport', // TODO(user): update this + instructions: 'Answer tech support questions.', // TODO(user): update this + description: 'A chatbot for quick tech-related queries.', // TODO(user): update this + metadata: { topic: 'Tech', language: 'en' }, // TODO(user): update this + }); + + await ainft.disconnect(); + + console.log(`\nSuccessfully created assistant with ID: ${assistant.id}`); + console.log(`assistant: ${JSON.stringify(assistant, null, 4)}`); + console.log(`txHash: ${tx_hash}`); + console.log(`\nView more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}/tokens/${tokenId}/ai`); + } catch (error) { + console.error('Failed to create assistant: ', error.message); + process.exit(1); + } +} + +main(); diff --git a/examples/chat/config.js b/examples/ai/config.js similarity index 59% rename from examples/chat/config.js rename to examples/ai/config.js index 882405d5..9da27ee5 100644 --- a/examples/chat/config.js +++ b/examples/ai/config.js @@ -2,19 +2,12 @@ // https://docs.ainetwork.ai/ainfts/developer-reference/ainft-tutorial/create-ainft-object-and-mint const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); +const { privateKey, objectId, appId } = require('../config.json'); // TODO(user): set these in config.json -['privateKey', 'objectId', 'appId'].forEach((key) => { - if (!config[key]?.trim()) { - throw new Error(`${key} is missing or empty in config.json`); - } -}); - -const { privateKey, objectId, appId } = config; // TODO(user): set these in config.json - -const ainft = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', chainId: 0, }); @@ -25,7 +18,7 @@ async function main() { const { config, tx_hash } = await ainft.chat.configure(objectId, 'openai'); console.log(`Successfully configured chat for ainft object!`); - console.log(`config: ${JSON.stringify(config, null, 2)}`); + console.log(`config: ${JSON.stringify(config, null, 4)}`); console.log(`txHash: ${tx_hash}`); console.log(`View more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}`); } catch (error) { diff --git a/examples/chat/deposit.js b/examples/ai/deposit.js similarity index 62% rename from examples/chat/deposit.js rename to examples/ai/deposit.js index eb9cc141..818f6654 100644 --- a/examples/chat/deposit.js +++ b/examples/ai/deposit.js @@ -1,15 +1,10 @@ const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); +const { privateKey } = require('../config.json'); // TODO(user): set this in config.json -if (!config.privateKey?.trim()) { - throw new Error('privateKey is missing or empty in config.json'); -} - -const { privateKey } = config; - -const ainft = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', chainId: 0, }); @@ -17,14 +12,17 @@ async function main() { try { console.log('Depositing credit...\n'); - // TODO(user): get testnet AIN from [faucet](https://faucet.ainetwork.ai) + await ainft.connect(); + const { tx_hash, address, balance } = await ainft.chat.depositCredit('openai', 10); + await ainft.disconnect(); + console.log(`\nSuccessfully deposited credit for chatting!`); console.log(`address: ${address}`); console.log(`balance: ${balance}`); console.log(`txHash: ${tx_hash}`); - console.log(`View more details at: https://testnet-insight.ainetwork.ai/transactions/${tx_hash}`); + console.log(`\nView more details at: https://testnet-insight.ainetwork.ai/transactions/${tx_hash}`); } catch (error) { console.error('Failed to deposit credit: ', error.message); process.exit(1); diff --git a/examples/ai/message.js b/examples/ai/message.js new file mode 100644 index 00000000..4234f4c9 --- /dev/null +++ b/examples/ai/message.js @@ -0,0 +1,38 @@ +// To run this example you must create a thread (see examples/chat/thread.js) + +const AinftJs = require('@ainft-team/ainft-js').default; +const { privateKey, objectId, appId, tokenId } = require('../config.json'); // TODO(user): set these in config.json + +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); + +async function main() { + try { + console.log('Creating message...\n'); + + await ainft.connect(); + + const threadId = ''; // TODO(user): update this + const { messages, tx_hash } = await ainft.message.create(objectId, tokenId, threadId, { + role: 'user', + content: 'What is git?', // TODO(user): update this + metadata: { language: 'en' }, // TODO(user): update this + }); + + await ainft.disconnect(); + + console.log(`\nSuccessfully created new message with reply:`); + console.log(`messages: ${JSON.stringify(messages, null, 4)}`); + console.log(`txHash: ${tx_hash}`); + console.log(`\nView more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}/tokens/${tokenId}/ai/history`); + } catch (error) { + console.error('Failed to create message: ', error.message); + process.exit(1); + } +} + +main(); diff --git a/examples/ai/thread.js b/examples/ai/thread.js new file mode 100644 index 00000000..512f4436 --- /dev/null +++ b/examples/ai/thread.js @@ -0,0 +1,31 @@ +const AinftJs = require('@ainft-team/ainft-js').default; +const { privateKey, objectId, appId, tokenId } = require('../config.json'); // TODO(user): set these in config.json + +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); + +async function main() { + try { + console.log('Creating thread...\n'); + + await ainft.connect(); + + const { thread, tx_hash } = await ainft.thread.create(objectId, tokenId, {}); + + await ainft.disconnect(); + + console.log(`\nSuccessfully created thread with ID: ${thread.id}`); + console.log(`thread: ${JSON.stringify(thread, null, 2)}`); + console.log(`txHash: ${tx_hash}`); + console.log(`\nView more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}/tokens/${tokenId}/ai/history`); + } catch (error) { + console.error('Failed to create thread: ', error.message); + process.exit(1); + } +} + +main(); diff --git a/examples/chat/assistant.js b/examples/chat/assistant.js deleted file mode 100644 index abd3e6a6..00000000 --- a/examples/chat/assistant.js +++ /dev/null @@ -1,44 +0,0 @@ -// To run this example, you must own an ainft object and token; create one if you don't. -// https://docs.ainetwork.ai/ainfts/developer-reference/ainft-tutorial/create-ainft-object-and-mint - -const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); - -['privateKey', 'objectId', 'appId', 'tokenId'].forEach((key) => { - if (!config[key]?.trim()) { - throw new Error(`${key} is missing or empty in config.json`); - } -}); - -const { privateKey, objectId, appId, tokenId } = config; // TODO(user): set these in config.json -const params = { - model: 'gpt-4', // TODO(user): update this - name: 'QuickSupport', // TODO(user): update this - instructions: 'Answer tech support questions.', // TODO(user): update this - description: 'A chatbot for quick tech-related queries.', // TODO(user): update this - metadata: { topic: 'Tech', language: 'en' }, // TODO(user): update this -}; - -const ainft = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, -}); - -async function main() { - try { - console.log('Creating assistant...\n'); - - const { assistant, tx_hash } = await ainft.chat.assistant.create(objectId, tokenId, 'openai', params); - - console.log(`\nSuccessfully created assistant with ID: ${assistant.id}`); - console.log(`assistant: ${JSON.stringify(assistant, null, 2)}`); - console.log(`txHash: ${tx_hash}`); - console.log(`View more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}/tokens/${tokenId}/ai`); - } catch (error) { - console.error('Failed to create assistant: ', error.message); - process.exit(1); - } -} - -main(); diff --git a/examples/chat/message.js b/examples/chat/message.js deleted file mode 100644 index 8041bbdf..00000000 --- a/examples/chat/message.js +++ /dev/null @@ -1,43 +0,0 @@ -// To run this example you must create a thread (see examples/chat/thread.js) - -const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); - -['privateKey', 'objectId', 'appId', 'tokenId'].forEach((key) => { - if (!config[key]?.trim()) { - throw new Error(`${key} is missing or empty in config.json`); - } -}); - -const { privateKey, objectId, appId, tokenId } = config; // TODO(user): set these in config.json -const threadId = ''; // TODO(user): update this -const params = { - role: 'user', - content: 'What is git?', // TODO(user): update this - metadata: { language: 'en' }, // TODO(user): update this -}; - -const ainft = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, -}); - -async function main() { - try { - console.log('Creating message...\n'); - - const { messages, tx_hash } = await ainft.chat.message.create(threadId, objectId, tokenId, 'openai', params); - - console.log(`\nSuccessfully created new message with reply:`); - console.log(`messages: ${JSON.stringify(messages, null, 2)}`); - console.log(`txHash: ${tx_hash}`); - // TODO(jiyoung): update service name in path - console.log(`View more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}/tokens/${tokenId}/ai/ainize_openai/history`); - } catch (error) { - console.error('Failed to create message: ', error.message); - process.exit(1); - } -} - -main(); diff --git a/examples/chat/thread.js b/examples/chat/thread.js deleted file mode 100644 index bb599ecd..00000000 --- a/examples/chat/thread.js +++ /dev/null @@ -1,35 +0,0 @@ -const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); - -['privateKey', 'objectId', 'appId', 'tokenId'].forEach((key) => { - if (!config[key]?.trim()) { - throw new Error(`${key} is missing or empty in config.json`); - } -}); - -const { privateKey, objectId, appId, tokenId } = config; // TODO(user): set these in config.json - -const ainft = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, -}); - -async function main() { - try { - console.log('Creating thread...\n'); - - const { thread, tx_hash } = await ainft.chat.thread.create(objectId, tokenId, 'openai', {}); - - console.log(`\nSuccessfully created thread with ID: ${thread.id}`); - console.log(`thread: ${JSON.stringify(thread, null, 2)}`); - console.log(`txHash: ${tx_hash}`); - // TODO(jiyoung): update service name in path - console.log(`View more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${appId}/tokens/${tokenId}/ai/ainize_openai/history`); - } catch (error) { - console.error('Failed to create thread: ', error.message); - process.exit(1); - } -} - -main(); diff --git a/examples/nft/createAinftObject.js b/examples/nft/createAinftObject.js index 5e3d15a8..48b25196 100644 --- a/examples/nft/createAinftObject.js +++ b/examples/nft/createAinftObject.js @@ -1,17 +1,14 @@ const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); +const { privateKey } = require('../config.json'); // TODO(user): set this in config.json -if (!config.privateKey?.trim()) { - throw new Error('privateKey is missing or empty in config.json'); -} - -const { privateKey } = config; // TODO(user): set this in config.json const name = 'MyObject'; // TODO(user): update this const symbol = 'MYOBJ'; // TODO(user): update this -const ainft = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, }); async function main() { @@ -24,7 +21,7 @@ async function main() { console.log(`objectId: ${ainftObject.id}`); console.log(`appId: ${ainftObject.appId}`); console.log(`txHash: ${txHash}`); - console.log(`View more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${ainftObject.appId}`); + console.log(`\nView more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${ainftObject.appId}`); } catch (error) { console.error('Failed to create ainft object: ', error.message); process.exit(1); diff --git a/examples/nft/mintNft.js b/examples/nft/mintNft.js index 545a90be..68f8e271 100644 --- a/examples/nft/mintNft.js +++ b/examples/nft/mintNft.js @@ -1,32 +1,27 @@ const AinftJs = require('@ainft-team/ainft-js').default; -const config = require('../config.json'); +const { privateKey, objectId } = require('../config.json'); // TODO(user): set these in config.json -['privateKey', 'objectId'].forEach((key) => { - if (!config[key]?.trim()) { - throw new Error(`${key} is missing or empty in config.json`); - } -}); - -const { privateKey, objectId } = config; // TODO(user): set these in config.json const to = '0x...'; // TODO(user): update this with recipient's ain address const tokenId = '1'; // TODO(user): update this as string to unique integer -const ainftJs = new AinftJs(privateKey, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, }); async function main() { try { console.log('Minting ainft token...\n'); - const ainftObject = await ainftJs.nft.get(objectId); + const ainftObject = await ainft.nft.get(objectId); const { tx_hash } = await ainftObject.mint(to, tokenId); console.log(`Successfully minted ainft token!`); console.log(`tokenId: ${tokenId}`); console.log(`txHash: ${tx_hash}`); - console.log(`View more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${ainftObject.appId}/tokens/${tokenId}`); + console.log(`\nView more details at: https://testnet-insight.ainetwork.ai/database/values/apps/${ainftObject.appId}/tokens/${tokenId}`); } catch (error) { console.error('Failed to mint ainft token: ', error.message); process.exit(1); diff --git a/examples/nft/retrieve.js b/examples/nft/retrieve.js index ef7e9751..a8178710 100644 --- a/examples/nft/retrieve.js +++ b/examples/nft/retrieve.js @@ -1,32 +1,37 @@ const AinftJs = require('@ainft-team/ainft-js').default; +const { address, privateKey, objectId } = require('../config.json'); // TODO(user): set these in config.json -const config = { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', -} +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); -const myPrivateKey = 'YOUR_PRIVATE_KEY'; -const ainftJs = new AinftJs(myPrivateKey, config); +const getAinftsByAccount = async (userAddress, limit, cursor) => { + return ainft.nft.getAinftsByAccount(userAddress, limit, cursor); +}; -const getAinftsByAccount = (userAddress, limit, cursor) => { - ainftJs.nft.getAinftsByAccount(userAddress, limit, cursor) - .then((res) => { - console.log(res); - }) - .catch((error) => { - console.log(error); - }); -} +const getAinftsByAinftObject = async (ainftObjectId, limit, cursor) => { + return ainft.nft.getAinftsByAinftObject(ainftObjectId, limit, cursor); +}; + +async function main() { + try { + console.log('Retrieving ainft tokens by account...\n'); + const result1 = await getAinftsByAccount(address); + console.log('Successfully retrieved ainft tokens by account!'); + console.log(JSON.stringify(result1, null, 4)); + console.log(); -const getAinftsByAinftObject = (ainftObjectId, limit, cursor) => { - ainftJs.nft.getAinftsByAinftObject(ainftObjectId, limit, cursor) - .then((res) => { - console.log(res); - }) - .catch((error) => { - console.log(error); - }); + console.log('Retrieving ainft tokens by object...\n'); + const result2 = await getAinftsByAinftObject(objectId); + console.log('Successfully retrieved ainft tokens by object!'); + console.log(JSON.stringify(result2, null, 4)); + } catch (error) { + console.error('Failed to retrieve ainft token: ', error.message); + process.exit(1); + } } -// getAinftsByAccount('YOUR_ACCOUNT_ADDRESS'); -// getAinftsByAinftObject('YOUR_AINFT_OBJECT_ID'); +main(); diff --git a/examples/nft/search.js b/examples/nft/search.js index ea6b53a0..182c3b6c 100644 --- a/examples/nft/search.js +++ b/examples/nft/search.js @@ -1,12 +1,12 @@ const AinftJs = require('@ainft-team/ainft-js').default; +const { privateKey } = require('../config.json'); -const config = { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', -} - -const myPrivateKey = 'YOUR_PRIVATE_KEY'; -const ainftJs = new AinftJs(myPrivateKey, config); +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); const searchAinftObjects = async () => { const filter = { @@ -16,15 +16,16 @@ const searchAinftObjects = async () => { symbol: '', limit: 0, cursor: '', - } - ainftJs.nft.searchAinftObjects(filter) + }; + ainft.nft + .searchAinftObjects(filter) .then((res) => { console.log(res); }) .catch((error) => { console.log(error); - }) -} + }); +}; const searchNfts = async () => { const filter = { @@ -35,15 +36,15 @@ const searchNfts = async () => { symbol: '', limit: 0, cursor: '', - } - ainftJs.nft.searchNfts(filter) + }; + ainft.nft + .searchNfts(filter) .then((res) => { console.log(res); - }) + }) .catch((error) => { console.log(error); }); -} - +}; -searchNfts(); \ No newline at end of file +searchNfts(); diff --git a/examples/nft/setMetadata.js b/examples/nft/setMetadata.js index eebaaf92..b8cfb3e4 100644 --- a/examples/nft/setMetadata.js +++ b/examples/nft/setMetadata.js @@ -1,29 +1,29 @@ const AinftJs = require('@ainft-team/ainft-js').default; +const { privateKey } = require('../config.json'); // TODO(user): set this in config.json -const myPrivateKey = 'YOUR_PRIVATE_KEY'; -const config = { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', -} -const ainftJs = new AinftJs(myPrivateKey, config); +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); -const ainftObjectId = 'YOUR_AINFT_OBJECT_ID'; +const objectId = 'YOUR_AINFT_OBJECT_ID'; const tokenId = '1'; const metadata = { name: 'my first token', - image: 'https://miro.medium.com/v2/resize:fit:2400/1*GWMy0ibykACFKS_rRxFlcw.png' -} - + image: 'https://miro.medium.com/v2/resize:fit:2400/1*GWMy0ibykACFKS_rRxFlcw.png', +}; const main = async () => { try { - const ainftObject = await ainftJs.nft.get(ainftObjectId); + const ainftObject = await ainft.nft.get(objectId); const ainft = await ainftObject.getToken(tokenId); const result = await ainft.setMetadata(metadata); - console.log(result); + console.log(result); } catch (error) { console.log(error); } -} +}; main(); diff --git a/examples/nft/transferNft.js b/examples/nft/transferNft.js index a2b27df7..4ea088ec 100644 --- a/examples/nft/transferNft.js +++ b/examples/nft/transferNft.js @@ -1,25 +1,26 @@ const AinftJs = require('@ainft-team/ainft-js').default; +const { privateKey } = require('../config.json'); // TODO(user): set this in config.json -const myPrivateKey = 'TOKEN_OWNER_PRIVATE_KEY'; -const config = { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', -} -const ainftJs = new AinftJs(myPrivateKey, config); - -const ainftObjectId = 'YOUR_AINFT_OBJECT_ID'; +const objectId = 'YOUR_AINFT_OBJECT_ID'; const from = 'TOKEN_OWNER_ADDRESS'; const to = 'RECEIVER_ADDRESS'; const tokenId = 'TOKEN_ID'; +const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, +}); + const main = async () => { try { - const ainftObject = await ainftJs.nft.get(ainftObjectId); + const ainftObject = await ainft.nft.get(objectId); const result = await ainftObject.transfer(from, to, tokenId); - console.log(result); + console.log(result); } catch (error) { console.log(error); } -} +}; main(); diff --git a/examples/package.json b/examples/package.json index aec79857..72d11df4 100644 --- a/examples/package.json +++ b/examples/package.json @@ -4,6 +4,6 @@ "main": "index.js", "license": "MIT", "dependencies": { - "@ainft-team/ainft-js": "2.0.0" + "@ainft-team/ainft-js": "2.1.0" } } diff --git a/examples/yarn.lock b/examples/yarn.lock index 8a05fffb..e60bebe8 100644 --- a/examples/yarn.lock +++ b/examples/yarn.lock @@ -2,53 +2,34 @@ # yarn lockfile v1 -"@ainblockchain/ain-js@^1.3.5": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.6.3.tgz#56ca744a6bf5e558f2acba75f106e8f88f5426ba" - integrity sha512-rdQfT6jcqcF4VP1twwMQkCijZ6SN1RewTjU1D35rJ7ZnRQjoIxekkodkdcIDVvyUEpR6A6iuT9SSSTz9KUMNbA== +"@ainblockchain/ain-js@^1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.10.0.tgz#2ab91ee9c5f083ef8830e8db528b69c18264aefa" + integrity sha512-4fmL7vaLBesY2de3aTtJTpjT0MO5ezeQSUlZcU5bjm5aY9LCFfCxSy3vkYrAROC7KstNODSfWjzMlpoc9afrWA== dependencies: "@ainblockchain/ain-util" "^1.1.9" "@types/node" "^12.7.3" "@types/randombytes" "^2.0.0" "@types/semver" "^7.3.4" + "@types/ws" "8.5.3" axios "^0.21.4" bip39 "^3.0.2" browserify-cipher "^1.0.1" eventemitter3 "^4.0.0" hdkey "^1.1.1" + is-in-browser "^2.0.0" + isomorphic-ws "^5.0.0" lodash "^4.17.20" node-seal "^4.5.7" + patch-package "^8.0.0" pbkdf2 "^3.0.17" + postinstall-postinstall "^2.1.0" randombytes "^2.1.0" scryptsy "^2.1.0" semver "^6.3.0" url-parse "^1.4.7" uuid "^3.3.3" - ws "^8.2.3" - -"@ainblockchain/ain-js@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.6.0.tgz#4a3fbefabe2afa28dfb2c71628c54310190d6c46" - integrity sha512-REzTJAf8w2TIsJLH7DhKWJF+kxfgMnCCwzWaeD4rYAv4TeD70PhFmYDrDMuy/qZd5KKMXqMigiU9PLWbiu8a7A== - dependencies: - "@ainblockchain/ain-util" "^1.1.9" - "@types/node" "^12.7.3" - "@types/randombytes" "^2.0.0" - "@types/semver" "^7.3.4" - axios "^0.21.4" - bip39 "^3.0.2" - browserify-cipher "^1.0.1" - eventemitter3 "^4.0.0" - hdkey "^1.1.1" - lodash "^4.17.20" - node-seal "^4.5.7" - pbkdf2 "^3.0.17" - randombytes "^2.1.0" - scryptsy "^2.1.0" - semver "^6.3.0" - url-parse "^1.4.7" - uuid "^3.3.3" - ws "^8.2.3" + ws "^8.16.0" "@ainblockchain/ain-util@^1.1.9": version "1.1.9" @@ -71,26 +52,26 @@ uuid "^3.3.3" varuint-bitcoin "^1.1.0" -"@ainft-team/ainft-js@1.1.0-beta.8": - version "1.1.0-beta.8" - resolved "https://registry.yarnpkg.com/@ainft-team/ainft-js/-/ainft-js-1.1.0-beta.8.tgz#5b8200c7340343bb86c18deac575359091ca2e6f" - integrity sha512-MMrz+8S2cIaRxP9RmZ7X9B+XlBsMpK0K0avx8MymknilPUVSOQ+yvVu/1uxnpMi30VdRxaWxgMpgF97bK6iw2g== +"@ainft-team/ainft-js@2.1.0-beta.14": + version "2.1.0-beta.14" + resolved "https://registry.yarnpkg.com/@ainft-team/ainft-js/-/ainft-js-2.1.0-beta.14.tgz#a6506fda42b87b414e8c9acb3637af4e8983e45b" + integrity sha512-hhqEKw8+L7o0+mIiraV1JiSuK+Bzp+MHtJaK/TfDCxt93pPAYalizOmHrAR1g3O9RO94zhXZwPDtx2665/TsEA== dependencies: - "@ainblockchain/ain-js" "^1.6.0" + "@ainblockchain/ain-js" "^1.10.0" "@ainblockchain/ain-util" "^1.1.9" - "@ainize-team/ainize-js" "1.0.4" + "@ainize-team/ainize-js" "^1.2.0" "@types/lodash" "^4.14.200" axios "^0.26.1" fast-json-stable-stringify "^2.1.0" form-data "^4.0.0" lodash "^4.17.21" -"@ainize-team/ainize-js@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@ainize-team/ainize-js/-/ainize-js-1.0.4.tgz#6be0c3c9cca27e4a71ced8c2f400fc4ba440ba27" - integrity sha512-D1e+LIyrv3NUWC17hcGRRTVacc+R5FeOcAhOIsPhpJvLwFDGbZgRl3zBgp7GDX2mfKyYEKyB4y9irtbyshtW/g== +"@ainize-team/ainize-js@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ainize-team/ainize-js/-/ainize-js-1.2.0.tgz#dd7945f0ba2e20a68989306069d5c5212a927a97" + integrity sha512-mBhtSJ4SAXw0vT+OOOs3EcBT5V2VpFq4qGf/1jQnbcGqVWuKHIS27hb7oTwVeUiVXaum6jIf5gWrrUrYkQsNoQ== dependencies: - "@ainblockchain/ain-js" "^1.3.5" + "@ainblockchain/ain-js" "^1.10.0" axios "^0.26.1" express "^4.18.2" fast-json-stable-stringify "^2.1.0" @@ -129,6 +110,18 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== +"@types/ws@8.5.3": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -142,6 +135,13 @@ acorn@7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -152,6 +152,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -166,6 +171,11 @@ axios@^0.26.1: dependencies: follow-redirects "^1.14.8" +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + base-x@^3.0.2: version "3.0.9" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" @@ -222,6 +232,21 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -284,7 +309,7 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.6: +call-bind@^1.0.5, call-bind@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -295,6 +320,19 @@ call-bind@^1.0.6: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -308,6 +346,18 @@ clone@2.x: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -315,6 +365,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -360,6 +415,15 @@ create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -530,6 +594,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -543,6 +614,13 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + follow-redirects@^1.14.0, follow-redirects@^1.14.8: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -567,6 +645,21 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-extra@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" @@ -583,6 +676,18 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -590,6 +695,16 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-property-descriptors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -677,7 +792,15 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -687,6 +810,67 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-in-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-2.0.0.tgz#a2343a18d8f8a600e8a20cb3022183a251e30355" + integrity sha512-/NUv5pqj+krUJalhGpj0lyy+x7vrD9jt1PlAfkoIDEXqE+xZgFJ4FU8e9m99WuHbCqsBZVf+nzvAjNso+SO80A== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== + +json-stable-stringify@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454" + integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg== + dependencies: + call-bind "^1.0.5" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + keccak@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/keccak/-/keccak-2.1.0.tgz#734ea53f2edcfd0f42cdb8d5f4c358fef052752b" @@ -697,6 +881,13 @@ keccak@^2.0.0: nan "^2.14.0" safe-buffer "^5.2.0" +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -726,6 +917,14 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromatch@^4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -753,6 +952,18 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -805,6 +1016,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -812,11 +1028,62 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -833,6 +1100,16 @@ pbkdf2@^3.0.17: safe-buffer "^5.0.1" sha.js "^2.4.8" +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -889,6 +1166,13 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -961,6 +1245,11 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.5.3: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -1015,6 +1304,18 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + side-channel@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.5.tgz#9a84546599b48909fb6af1211708d23b1946221b" @@ -1025,6 +1326,11 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -1037,6 +1343,27 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -1050,6 +1377,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -1090,7 +1422,24 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -ws@^8.2.3: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.16.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + +yaml@^2.2.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" + integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== diff --git a/package.json b/package.json index 9a387561..d90f914b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@ainft-team/ainft-js", "main": "dist/ainft.js", "types": "dist/ainft.d.ts", - "version": "2.0.2", + "version": "2.1.0", "engines": { "node": ">=16" }, @@ -39,9 +39,9 @@ "typescript": "^4.6.3" }, "dependencies": { - "@ainblockchain/ain-js": "^1.10.2", + "@ainblockchain/ain-js": "^1.13.0", "@ainblockchain/ain-util": "^1.1.9", - "@ainize-team/ainize-js": "^1.1.1", + "@ainize-team/ainize-js": "^1.3.1", "@types/lodash": "^4.14.200", "axios": "^0.26.1", "fast-json-stable-stringify": "^2.1.0", diff --git a/sample/index.js b/sample/index.js deleted file mode 100644 index 636653a3..00000000 --- a/sample/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/* Contains sample code for the SDK. */ -const AinftJs = require('@ainft-team/ainft-js'); - -async function main() { - const ainftJs = new AinftJs(); - const status = await ainftJs.getStatus(); - console.log('Got status: ', status); -} - -main(); diff --git a/sample/package.json b/sample/package.json deleted file mode 100644 index 78cd68bd..00000000 --- a/sample/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "sample", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "dependencies": { - "@ainft-team/ainft-js": "^0.0.0" - } -} diff --git a/sample/yarn.lock b/sample/yarn.lock deleted file mode 100644 index 9b7c4d52..00000000 --- a/sample/yarn.lock +++ /dev/null @@ -1,22 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ainft-team/ainft-js@0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@ainft-team/ainft-js/-/ainft-js-0.0.0.tgz#e45134960b9dbeb2b2548267d8387ec38622ac58" - integrity sha512-tWHwmtwFdQvP2MW66DIOXYDW8oi4hitocHICvbO/l2VSQmX+mvHwlxyNH0bevM3pG8K1ztxKe/dgk7XSAJVR8Q== - dependencies: - axios "^0.26.1" - -axios@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== - dependencies: - follow-redirects "^1.14.8" - -follow-redirects@^1.14.8: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== diff --git a/src/activity.ts b/src/activity.ts index 562d972c..0917f4b0 100644 --- a/src/activity.ts +++ b/src/activity.ts @@ -1,5 +1,6 @@ import FactoryBase from "./factoryBase"; import { ActivityNftInfo, AddAiHistoryParams, HttpMethod, NftActivityType, TaskTypeCategory, getTxbodyAddAiHistoryParams } from "./types"; +import { authenticated } from "./utils/decorator"; /** * This class supports add activities.\ @@ -14,6 +15,7 @@ export default class Activity extends FactoryBase { * @param activityType The type of activity. * @param nftInfo Information about the NFTs used in the activity. */ + @authenticated add( appId: string, userId: string, @@ -40,6 +42,7 @@ export default class Activity extends FactoryBase { * @param label The label of record. * @param data Data related to the activity. */ + @authenticated addNftRecord( appId: string, userId: string, @@ -63,6 +66,7 @@ export default class Activity extends FactoryBase { * @param {AddAiHistoryParams} AddAiHistoryParams The paramters to add ai history. * @returns */ + @authenticated async addNftAiHistory({ chain, network, @@ -92,6 +96,7 @@ export default class Activity extends FactoryBase { * @param {getTxbodyAddAiHistoryParams} getTxbodyAddAiHistoryParams * @returns Returns transaction body without signature. */ + @authenticated getTxBodyForAddNftAiHistory({ chain, network, diff --git a/src/ai/ai.ts b/src/ai/ai.ts new file mode 100644 index 00000000..21b05cb1 --- /dev/null +++ b/src/ai/ai.ts @@ -0,0 +1,181 @@ +import _ from 'lodash'; +import Service from '@ainize-team/ainize-js/dist/service'; + +import FactoryBase from '../factoryBase'; +import AinftObject from '../ainft721Object'; +import { OperationType, getService, request } from '../utils/ainize'; +import { + AiConfigurationTransactionResult, + CreditTransactionResult, + NftToken, + NftTokens, + QueryParamsWithoutSort, + TokenStatus, +} from '../types'; +import { DEFAULT_AINIZE_SERVICE_NAME } from '../constants'; +import { Path } from '../utils/path'; +import { buildSetTxBody, buildSetValueOp, sendTx } from '../utils/transaction'; +import { sleep } from '../utils/util'; +import { validateObject, validateObjectOwner } from '../utils/validator'; +import { authenticated } from '../utils/decorator'; +import { AinftError } from '../error'; + +/** + * This class manages ai configurations for AINFT object,\ + * manages the credits needed for their uses, and checks if tokens are available for creation.\ + * Do not create it directly; Get it from AinftJs instance. + */ +export class Ai extends FactoryBase { + /** + * Sets up ai configuration for an AINFT object. + * @param {string} objectId - The ID of the AINFT object. + * @param {string} serviceName - The name of Ainize service. + * @returns {Promise} A promise that resolves with both the transaction result and the configuration info. + */ + @authenticated + async configure( + objectId: string, + serviceName: string + ): Promise { + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateObjectOwner(this.ain, objectId, address); + await getService(this.ainize!, serviceName); // NOTE(jiyoung): check if the service is deployed on Ainize. + + const txBody = this.buildTxBodyForConfigureAi(objectId, serviceName, address); + const result = await sendTx(txBody, this.ain); + + return { ...result, config: { name: serviceName } }; + } + + /** + * Retrieves the credit balance for a service. + * @param {string} serviceName - The name of Ainize service. + * @returns {Promise} A promise that resolves with the credit balance. + */ + @authenticated + async getCredit(serviceName: string): Promise { + const address = await this.ain.signer.getAddress(); + + let balance = 0; + + if (serviceName === DEFAULT_AINIZE_SERVICE_NAME) { + const opType = OperationType.GET_CREDIT; + const body = { address }; + + const response = await request(this.ainize!, { + serviceName: serviceName, + opType, + data: body, + timeout: 2 * 60 * 1000, // 2min + }); + balance = response.data; + } else { + const service = await getService(this.ainize!, serviceName); + balance = await service.getCreditBalance(); + } + + return balance; + } + + /** + * @todo Enable deposit function once withdrawal is fully implemented and tested. + * Deposits a credits for a service. + * @param {string} serviceName - The name of Ainize service. + * @param {number} amount - The amount of credits to deposit. + * @returns {Promise} A promise that resolves with the deposit transaction info. + */ + /* + @authenticated + async depositCredit(serviceName: string, amount: number): Promise { + const address = await this.ain.signer.getAddress(); + + const service = await getService(this.ainize!, serviceName); // NOTE(jiyoung): check if the service is deployed on Ainize. + + const currentBalance = await service.getCreditBalance(); + const txHash = await service.chargeCredit(amount); + const updatedBalance = await this.waitForUpdate( + service, + currentBalance + amount, + 60*1000, // 1min + txHash + ); + + return { tx_hash: txHash, address, balance: updatedBalance }; + } + */ + + // TODO(jiyoung): refactor this method. + async getUserTokensByStatus( + objectId: string, + address: string, + status?: string | null, + { limit = 20, offset = 0, order = 'desc' }: QueryParamsWithoutSort = {} + ) { + await validateObject(this.ain, objectId); + + const appId = AinftObject.getAppId(objectId); + const tokensPath = Path.app(appId).tokens().value(); + const allTokens: NftTokens = (await this.ain.db.ref(tokensPath).getValue()) || {}; + + const tokens = Object.entries(allTokens).reduce((acc, [id, token]) => { + if (token.owner === address) { + acc.push({ tokenId: id, ...token }); + } + return acc; + }, []); + + tokens.map((token) => { + let status = TokenStatus.MINTED; + const assistantCreated = token.ai; + if (assistantCreated) { + status = TokenStatus.ASSISTANT_CREATED; + const threadCreated = _.some(token.ai.history, (address) => !_.isEmpty(address.threads)); + if (threadCreated) { + status = TokenStatus.THREAD_CREATED; + const messageCreated = _.some(token.ai.history, (address) => + _.some( + address.threads, + (thread) => _.isObject(thread.messages) && !_.isEmpty(thread.messages) + ) + ); + if (messageCreated) { + status = TokenStatus.MESSAGE_CREATED; + } + } + } + token.status = status; + return token; + }); + + const filtered = tokens.filter((token) => !status || token.status == status); + const sorted = _.orderBy(filtered, ['tokenId'], [order]); + + const total = sorted.length; + const items = sorted.slice(offset, offset + limit); + + return { total, items }; + } + + private buildTxBodyForConfigureAi(objectId: string, serviceName: string, address: string) { + const appId = AinftObject.getAppId(objectId); + const path = Path.app(appId).ai(serviceName).value(); + return buildSetTxBody(buildSetValueOp(path, { name: serviceName }), address); + } + + private async waitForUpdate(service: Service, expected: number, timeout: number, txHash: string) { + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + const balance = await service.getCreditBalance(); + if (balance === expected) { + return balance; + } + await sleep(1000); // 1sec + } + throw new AinftError( + 'gateway-timeout', + `timeout of ${timeout}ms exceeded. please check the transaction status: ${txHash}` + ); + } +} diff --git a/src/ai/assistant.ts b/src/ai/assistant.ts new file mode 100644 index 00000000..4697c3d4 --- /dev/null +++ b/src/ai/assistant.ts @@ -0,0 +1,464 @@ +import _ from 'lodash'; + +import FactoryBase from '../factoryBase'; +import AinftObject from '../ainft721Object'; +import { OperationType, getServiceName, request } from '../utils/ainize'; +import { + Assistant, + AssistantCreateOptions, + AssistantCreateParams, + AssistantDeleteTransactionResult, + AssistantDeleted, + AssistantTransactionResult, + AssistantUpdateParams, + NftToken, + NftTokens, + QueryParamsWithoutSort, +} from '../types'; +import { + MESSAGE_GC_MAX_SIBLINGS, + MESSAGE_GC_NUM_SIBLINGS_DELETED, + THREAD_GC_MAX_SIBLINGS, + THREAD_GC_NUM_SIBLINGS_DELETED, + WHITELISTED_OBJECT_IDS, +} from '../constants'; +import { getEnv } from '../utils/env'; +import { Path } from '../utils/path'; +import { + buildSetValueOp, + buildSetWriteRuleOp, + buildSetStateRuleOp, + buildSetOp, + buildSetTxBody, + sendTx, +} from '../utils/transaction'; +import { getChecksumAddress, getAssistant, getToken } from '../utils/util'; +import { + isObjectOwner, + validateAssistant, + validateDuplicateAssistant, + validateObject, + validateServerConfigurationForObject, + validateToken, +} from '../utils/validator'; +import { authenticated } from '../utils/decorator'; +import { AinftError } from '../error'; + +enum Role { + OWNER = 'owner', + USER = 'user', +} + +/** + * This class supports building assistants that enables conversation with LLM models.\ + * Do not create it directly; Get it from AinftJs instance. + */ +export class Assistants extends FactoryBase { + /** + * Create an assistant with a model and instructions. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {AssistantCreateParams} AssistantCreateParams - The parameters to create assistant. + * @param {AssistantCreateOptions} AssistantCreateOptions - The creation options. + * @returns A promise that resolves with both the transaction result and the created assistant. + */ + @authenticated + async create( + objectId: string, + tokenId: string, + { model, name, instructions, description, metadata }: AssistantCreateParams, + options: AssistantCreateOptions = {} + ): Promise { + const address = await this.ain.signer.getAddress(); + + // TODO(jiyoung): limit character count for 'instruction' and 'description'. + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateDuplicateAssistant(this.ain, objectId, tokenId); + + // NOTE(jiyoung): creation is limited to owners, except for whitelisted objects. + const role = (await isObjectOwner(this.ain, objectId, address)) ? Role.OWNER : Role.USER; + const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); + if (!whitelisted && role !== Role.OWNER) { + throw new AinftError('permission-denied', `cannot create assistant for ${objectId}`); + } + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.CREATE_ASSISTANT; + const body = { + role, + objectId, + tokenId, + model, + name, + instructions, + ...(description && { description }), + ...(metadata && !_.isEmpty(metadata) && { metadata }), + ...(options && !_.isEmpty(options) && { options }), + }; + + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + if (role === Role.OWNER) { + const txBody = this.buildTxBodyForCreateAssistant(address, objectId, tokenId, data); + const result = await sendTx(txBody, this.ain); + return { ...result, assistant: data }; + } else { + return { assistant: data }; + } + } + + /** + * Updates an assistant. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} assistantId - The ID of assistant. + * @param {AssistantUpdateParams} AssistantUpdateParams - The parameters to update assistant. + * @returns A promise that resolves with both the transaction result and the updated assistant. + */ + @authenticated + async update( + objectId: string, + tokenId: string, + assistantId: string, + { model, name, instructions, description, metadata }: AssistantUpdateParams + ): Promise { + const address = await this.ain.signer.getAddress(); + + // TODO(jiyoung): limit character count for 'instruction' and 'description'. + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId, assistantId); + + // NOTE(jiyoung): update is limited to owners, except for whitelisted objects. + const role = (await isObjectOwner(this.ain, objectId, address)) ? Role.OWNER : Role.USER; + const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); + if (!whitelisted && role !== Role.OWNER) { + throw new AinftError('permission-denied', `cannot update assistant for ${objectId}`); + } + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.MODIFY_ASSISTANT; + const body = { + role, + objectId, + tokenId, + assistantId, + ...(model && { model }), + ...(name && { name }), + ...(instructions && { instructions }), + ...(description && { description }), + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + if (role === Role.OWNER) { + const txBody = this.buildTxBodyForUpdateAssistant(address, objectId, tokenId, data); + const result = await sendTx(txBody, this.ain); + return { ...result, assistant: data }; + } else { + return { assistant: data }; + } + } + + /** + * Deletes an assistant. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} assistantId - The ID of assistant. + * @returns A promise that resolves with both the transaction result and the deleted assistant. + */ + @authenticated + async delete( + objectId: string, + tokenId: string, + assistantId: string + ): Promise { + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId, assistantId); + + // NOTE(jiyoung): deletion is limited to owners, except for whitelisted objects. + const role = (await isObjectOwner(this.ain, objectId, address)) ? Role.OWNER : Role.USER; + const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); + if (!whitelisted && role !== Role.OWNER) { + throw new AinftError('permission-denied', `cannot delete assistant for ${objectId}`); + } + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.DELETE_ASSISTANT; + const body = { role, objectId, tokenId, assistantId }; + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + if (role === Role.OWNER) { + const txBody = this.buildTxBodyForDeleteAssistant(address, objectId, tokenId); + const result = await sendTx(txBody, this.ain); + return { ...result, delAssistant: data }; + } else { + return { delAssistant: data }; + } + } + + /** + * Retrieves an assistant. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} assistantId - The ID of assistant. + * @returns A promise that resolves with the assistant. + */ + async get(objectId: string, tokenId: string, assistantId: string): Promise { + const appId = AinftObject.getAppId(objectId); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + + const assistant = await getAssistant(this.ain, appId, tokenId); + const token = await getToken(this.ain, appId, tokenId); + + const data = { + id: assistant.id, + objectId, + tokenId, + owner: token.owner, + model: assistant.config.model, + name: assistant.config.name, + instructions: assistant.config.instructions, + description: assistant.config.description || null, + metadata: assistant.config.metadata || {}, + created_at: assistant.createdAt, + }; + + return data; + } + + /** + * Retrieves a list of assistants. + * @param {string[]} objectIds - The ID(s) of AINFT object. + * @param {string} [address] - (Optional) The checksum address of account. + * @param {QueryParamsWithoutSort} [queryParamsWithoutSort={}] - The parameters for querying items. + * @returns A promise that resolves with the list of assistants. + */ + async list( + objectIds: string[], + address?: string | null, + { limit = 20, offset = 0, order = 'desc' }: QueryParamsWithoutSort = {} + ) { + await Promise.all(objectIds.map((objectId) => validateObject(this.ain, objectId))); + + const allAssistants = await Promise.all( + objectIds.map(async (objectId) => { + const tokens = await this.getTokens(objectId, address); + return this.getAssistantsFromTokens(tokens); + }) + ); + const assistants = allAssistants.flat(); + + const sorted = this.sortAssistants(assistants, order); + + const total = sorted.length; + const items = sorted.slice(offset, offset + limit); + + return { total, items }; + } + + @authenticated + async mint(objectId: string, to: string) { + const checksum = getChecksumAddress(to); + const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); + if (!whitelisted) { + throw new AinftError( + 'forbidden', + `cannot mint token for ${objectId}. please use the Ainft721Object.mint() function instead.` + ); + } + + const serviceName = getServiceName(); + const opType = OperationType.MINT_TOKEN; + const body = { objectId, to: checksum }; + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + timeout: 2 * 60 * 1000, // 2min + }); + + return data; + } + + private buildTxBodyForCreateAssistant( + address: string, + objectId: string, + tokenId: string, + { id, model, name, instructions, description, metadata, created_at }: Assistant + ) { + const appId = AinftObject.getAppId(objectId); + const assistantPath = Path.app(appId).token(tokenId).ai().value(); + const historyPath = `/apps/${appId}/tokens/${tokenId}/ai/history/$user_addr`; + const threadPath = `${historyPath}/threads/$thread_id`; + const messagePath = `${threadPath}/messages/$message_id`; + + const config = { + model, + name, + instructions, + ...(description && { description }), + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + const info = { + id, + type: 'chat', + config, + createdAt: created_at, + history: true, + }; + + const rules = { + write: `auth.addr === $user_addr || util.isAppAdmin('${appId}', auth.addr, getValue) === true`, + state: { + thread: { + gc_max_siblings: THREAD_GC_MAX_SIBLINGS, + gc_num_siblings_deleted: THREAD_GC_NUM_SIBLINGS_DELETED, + }, + message: { + gc_max_siblings: MESSAGE_GC_MAX_SIBLINGS, + gc_num_siblings_deleted: MESSAGE_GC_NUM_SIBLINGS_DELETED, + }, + }, + }; + + const setAssistantInfoOp = buildSetValueOp(assistantPath, info); + const setHistoryWriteRuleOp = buildSetWriteRuleOp(historyPath, rules.write); + const setThreadGCRuleOp = buildSetStateRuleOp(threadPath, rules.state.thread); + const setMessageGCRuleOp = buildSetStateRuleOp(messagePath, rules.state.message); + + return buildSetTxBody( + buildSetOp([ + setAssistantInfoOp, + setHistoryWriteRuleOp, + setThreadGCRuleOp, + setMessageGCRuleOp, + ]), + address + ); + } + + private buildTxBodyForUpdateAssistant( + address: string, + objectId: string, + tokenId: string, + { model, name, instructions, description, metadata }: Assistant + ) { + const appId = AinftObject.getAppId(objectId); + const assistantConfigPath = Path.app(appId).token(tokenId).ai().config().value(); + + const config = { + model, + name, + instructions, + ...(description && { description }), + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + + return buildSetTxBody(buildSetValueOp(assistantConfigPath, config), address); + } + + private buildTxBodyForDeleteAssistant(address: string, objectId: string, tokenId: string) { + const appId = AinftObject.getAppId(objectId); + const assistantPath = Path.app(appId).token(tokenId).ai().value(); + const historyPath = `/apps/${appId}/tokens/${tokenId}/ai/history/$user_addr`; + const threadPath = `${historyPath}/threads/$thread_id`; + const messagePath = `${threadPath}/messages/$message_id`; + + const unsetMessageGCRuleOp = buildSetStateRuleOp(messagePath, null); + const unsetThreadGCRuleOp = buildSetStateRuleOp(threadPath, null); + const unsetHistoryWriteRuleOp = buildSetWriteRuleOp(historyPath, null); + const unsetAssistantInfoOp = buildSetValueOp(assistantPath, null); + + return buildSetTxBody( + buildSetOp([ + unsetMessageGCRuleOp, + unsetThreadGCRuleOp, + unsetHistoryWriteRuleOp, + unsetAssistantInfoOp, + ]), + address + ); + } + + private async getTokens(objectId: string, address?: string | null) { + const appId = AinftObject.getAppId(objectId); + const tokensPath = Path.app(appId).tokens().value(); + const tokens: NftTokens = (await this.ain.db.ref(tokensPath).getValue()) || {}; + return Object.entries(tokens).reduce((acc, [id, token]) => { + if (!address || token.owner === address) { + acc.push({ objectId, tokenId: id, ...token }); + } + return acc; + }, []); + } + + private getAssistantsFromTokens(tokens: NftToken[]) { + return tokens.reduce((acc, token) => { + if (token.ai) { + acc.push(this.toAssistant(token, this.countThreads(token.ai.history))); + } + return acc; + }, []); + } + + private toAssistant(data: any, numThreads: number): Assistant { + return { + id: data.ai.id, + objectId: data.objectId, + tokenId: data.tokenId, + owner: data.owner, + model: data.ai.config.model, + name: data.ai.config.name, + instructions: data.ai.config.instructions, + description: data.ai.config.description || null, + metadata: data.ai.config.metadata || {}, + created_at: data.ai.createdAt, + metric: { numThreads }, + }; + } + + private sortAssistants(assistants: Assistant[], order: 'asc' | 'desc') { + return assistants.sort((a, b) => { + if (a.created_at < b.created_at) return order === 'asc' ? -1 : 1; + if (a.created_at > b.created_at) return order === 'asc' ? 1 : -1; + return 0; + }); + } + + private countThreads(items: any) { + if (typeof items !== 'object' || !items) { + return 0; + } + return Object.values(items).reduce((sum: number, item: any) => { + const count = + item.threads && typeof item.threads === 'object' ? Object.keys(item.threads).length : 0; + return sum + count; + }, 0); + } +} diff --git a/src/ai/index.ts b/src/ai/index.ts new file mode 100644 index 00000000..327b3e55 --- /dev/null +++ b/src/ai/index.ts @@ -0,0 +1,4 @@ +export { Ai } from './ai'; +export { Assistants } from './assistant'; +export { Threads } from './thread'; +export { Messages } from './message'; diff --git a/src/ai/message.ts b/src/ai/message.ts new file mode 100644 index 00000000..0a1c858e --- /dev/null +++ b/src/ai/message.ts @@ -0,0 +1,343 @@ +import _ from 'lodash'; + +import FactoryBase from '../factoryBase'; +import AinftObject from '../ainft721Object'; +import { OperationType, getServiceName, request } from '../utils/ainize'; +import { + Message, + MessageCreateParams, + MessagesTransactionResult, + MessageMap, + MessageTransactionResult, + MessageUpdateParams, +} from '../types'; +import { Path } from '../utils/path'; +import { buildSetValueOp, buildSetOp, buildSetTxBody, sendTx } from '../utils/transaction'; +import { getAssistant, getValue } from '../utils/util'; +import { + validateAssistant, + validateMessage, + validateObject, + validateServerConfigurationForObject, + validateThread, + validateToken, +} from '../utils/validator'; +import { authenticated } from '../utils/decorator'; +import { AinftError } from '../error'; + +/** + * This class supports create messages within threads.\ + * Do not create it directly; Get it from AinftJs instance. + */ +export class Messages extends FactoryBase { + /** + * Create a message. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @param {MessageCreateParams} MessageCreateParams - The parameters to create message. + * @returns A promise that resolves with both the transaction result and a list including the new message. + */ + @authenticated + async create( + objectId: string, + tokenId: string, + threadId: string, + body: MessageCreateParams + ): Promise { + const appId = AinftObject.getAppId(objectId); + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const assistant = await getAssistant(this.ain, appId, tokenId); + const newMessages = await this.sendMessage(serviceName, threadId, objectId, tokenId, assistant.id, address, body); + const allMessages = await this.getAllMessages(appId, tokenId, address, threadId, newMessages); + + const txBody = await this.buildTxBodyForCreateMessage( + address, + objectId, + tokenId, + threadId, + allMessages + ); + const result = await sendTx(txBody, this.ain); + + return { ...result, messages: allMessages }; + } + + /** + * Updates a thread. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @param {string} messageId - The ID of message. + * @param {MessageUpdateParams} MessageUpdateParams - The parameters to update message. + * @returns A promise that resolves with both the transaction result and the updated message. + */ + @authenticated + async update( + objectId: string, + tokenId: string, + threadId: string, + messageId: string, + { metadata }: MessageUpdateParams + ): Promise { + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + await validateMessage(this.ain, objectId, tokenId, address, threadId, messageId); + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.MODIFY_MESSAGE; + const body = { + threadId, + messageId, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + const txBody = await this.buildTxBodyForUpdateMessage(data, objectId, tokenId, address); + const result = await sendTx(txBody, this.ain); + + return { ...result, message: data }; + } + + /** + * Retrieves a message. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @param {string} messageId - The ID of message. + * @param {string} address - The checksum address of account. + * @returns A promise that resolves with the message. + */ + async get( + objectId: string, + tokenId: string, + threadId: string, + messageId: string, + address: string + ): Promise { + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + + const appId = AinftObject.getAppId(objectId); + const messagesPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .messages() + .value(); + const messages: MessageMap = await getValue(this.ain, messagesPath); + const key = this.findMessageKey(messages, messageId); + + return messages[key]; + } + + /** + * Retrieves a list of messages. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @param {string} address - The checksum address of account. + * @returns A promise that resolves with the list of messages. + */ + async list( + objectId: string, + tokenId: string, + threadId: string, + address: string + ): Promise { + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + + const appId = AinftObject.getAppId(objectId); + const messagesPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .messages() + .value(); + const messages = await this.ain.db.ref(messagesPath).getValue(); + + return messages; + } + + private async sendMessage( + serviceName: string, + threadId: string, + objectId: string, + tokenId: string, + assistantId: string, + address: string, + params: MessageCreateParams + ) { + try { + const opType = OperationType.SEND_MESSAGE; + const body = { ...params, threadId, objectId, tokenId, assistantId, address }; + const { data } = await request(this.ainize!, { serviceName, opType, data: body }); + return data.data; + } catch (error: any) { + throw new AinftError('internal', error.message); + } + } + + private async buildTxBodyForCreateMessage( + address: string, + objectId: string, + tokenId: string, + threadId: string, + messages: any + ) { + const appId = AinftObject.getAppId(objectId); + const messagesPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .messages() + .value(); + const threadPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .value(); + + const prev = (await this.ain.db.ref(`${threadPath}/metadata`).getValue()) || {}; + + const messageKeys = Object.keys(messages); + const lastMessageKey = messageKeys[messageKeys.length - 1]; + + const defaultTitle = 'New chat'; + const lastMessage = messages[lastMessageKey]?.content[0]?.text?.value || defaultTitle; + + const maxLength = 10; + const title = + lastMessage.length > maxLength ? lastMessage.substring(0, maxLength) + '...' : lastMessage; + + const setMessageInfoOp = buildSetValueOp(messagesPath, messages); + const setThreadTitleOp = buildSetValueOp(`${threadPath}/metadata`, { + ...prev, + title, + }); + + return buildSetTxBody(buildSetOp([setThreadTitleOp, setMessageInfoOp]), address); + } + + private async buildTxBodyForUpdateMessage( + { id, thread_id, metadata }: Message, + objectId: string, + tokenId: string, + address: string + ) { + const appId = AinftObject.getAppId(objectId); + const messagesPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(thread_id) + .messages() + .value(); + + let key = null; + const messages: MessageMap = await getValue(this.ain, messagesPath); + for (const ts in messages) { + if (messages[ts].id === id) { + key = ts; + break; + } + } + if (!key) { + throw new AinftError('not-found', `message not found: ${id}`); + } + + const messagePath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(thread_id) + .message(key) + .value(); + const prev = await getValue(this.ain, messagePath); + const value = { + ...prev, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + + return buildSetTxBody(buildSetValueOp(messagePath, value), address); + } + + private async getAllMessages( + appId: string, + tokenId: string, + address: string, + threadId: string, + newMessages: MessageMap + ) { + let messages: { [key: string]: any } = {}; + const messagesPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .messages() + .value(); + + const prev = await this.ain.db.ref(messagesPath).getValue(); + if (_.isObject(prev) && !_.isEmpty(prev)) { + messages = { + ...prev, + }; + } + Object.keys(newMessages).forEach((key) => { + const { id, created_at, role, content, metadata } = newMessages[key]; + messages[`${created_at}`] = { + id, + role, + createdAt: created_at, + ...(content && !_.isEmpty(content) && { content }), + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + }); + + return messages; + } + + private findMessageKey = (messages: MessageMap, messageId: string) => { + let messageKey = null; + for (const key in messages) { + if (messages[key].id === messageId) { + messageKey = key; + break; + } + } + if (!messageKey) { + throw new AinftError('not-found', `message not found: ${messageId}`); + } + return messageKey; + }; +} diff --git a/src/ai/thread.ts b/src/ai/thread.ts new file mode 100644 index 00000000..450d1867 --- /dev/null +++ b/src/ai/thread.ts @@ -0,0 +1,353 @@ +import _ from 'lodash'; + +import FactoryBase from '../factoryBase'; +import AinftObject from '../ainft721Object'; +import { OperationType, getServiceName, request } from '../utils/ainize'; +import { + QueryParams, + Thread, + ThreadCreateParams, + ThreadDeleteTransactionResult, + ThreadDeleted, + ThreadTransactionResult, + ThreadUpdateParams, + ThreadWithMessages, +} from '../types'; +import { Path } from '../utils/path'; +import { buildSetValueOp, buildSetTxBody, sendTx } from '../utils/transaction'; +import { getAssistant, getValue } from '../utils/util'; +import { + validateAssistant, + validateObject, + validateServerConfigurationForObject, + validateThread, + validateToken, +} from '../utils/validator'; +import { authenticated } from '../utils/decorator'; +import { AinftError } from '../error'; + +/** + * This class supports create threads that assistant can interact with.\ + * Do not create it directly; Get it from AinftJs instance. + */ +export class Threads extends FactoryBase { + /** + * Create a thread. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {ThreadCreateParams} ThreadCreateParams - The parameters to create thread. + * @returns A promise that resolves with both the transaction result and the created thread. + */ + @authenticated + async create( + objectId: string, + tokenId: string, + { metadata }: ThreadCreateParams + ): Promise { + const appId = AinftObject.getAppId(objectId); + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + const assistant = await getAssistant(this.ain, appId, tokenId); + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.CREATE_THREAD; + const body = { + objectId, + tokenId, + assistant: { + id: assistant?.id, + model: assistant?.config?.model, + name: assistant?.config?.name, + instructions: assistant?.config?.instructions, + description: assistant?.config?.description || null, + metadata: assistant?.config?.metadata || null, + createdAt: assistant?.createdAt, + }, + address, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + const txBody = this.buildTxBodyForCreateThread(address, objectId, tokenId, data); + const result = await sendTx(txBody, this.ain); + + return { ...result, thread: data, tokenId }; + } + + /** + * Updates a thread. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @param {ThreadUpdateParams} ThreadUpdateParams - The parameters to update thread. + * @returns A promise that resolves with both the transaction result and the updated thread. + */ + @authenticated + async update( + objectId: string, + tokenId: string, + threadId: string, + { metadata }: ThreadUpdateParams + ): Promise { + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.MODIFY_THREAD; + const body = { + threadId, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + const txBody = await this.buildTxBodyForUpdateThread(address, objectId, tokenId, data); + const result = await sendTx(txBody, this.ain); + + return { ...result, thread: data }; + } + + /** + * Deletes a thread. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @returns A promise that resolves with both the transaction result and the deleted thread. + */ + @authenticated + async delete( + objectId: string, + tokenId: string, + threadId: string + ): Promise { + const address = await this.ain.signer.getAddress(); + + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + + const serviceName = getServiceName(); + await validateServerConfigurationForObject(this.ain, objectId, serviceName); + + const opType = OperationType.DELETE_THREAD; + const body = { threadId }; + + const { data } = await request(this.ainize!, { + serviceName, + opType, + data: body, + }); + + const txBody = this.buildTxBodyForDeleteThread(address, objectId, tokenId, threadId); + const result = await sendTx(txBody, this.ain); + + return { ...result, delThread: data }; + } + + /** + * Retrieves a thread. + * @param {string} objectId - The ID of AINFT object. + * @param {string} tokenId - The ID of AINFT token. + * @param {string} threadId - The ID of thread. + * @param {string} address - The checksum address of account. + * @returns A promise that resolves with the thread. + */ + async get(objectId: string, tokenId: string, threadId: string, address: string): Promise { + await validateObject(this.ain, objectId); + await validateToken(this.ain, objectId, tokenId); + await validateAssistant(this.ain, objectId, tokenId); + await validateThread(this.ain, objectId, tokenId, address, threadId); + + const appId = AinftObject.getAppId(objectId); + const threadPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .value(); + const data = await this.ain.db.ref(threadPath).getValue(); + const thread = { + id: data.id, + metadata: data.metadata || {}, + created_at: data.createdAt, + }; + + return thread; + } + + /** + * Retrieves a list of threads. + * @param {string[]} objectIds - The ID(s) of AINFT object. + * @param {string | null} [tokenId] - The ID of AINFT token. + * @param {string | null} [address] - The checksum address of account. + * @param {QueryParams} QueryParams - The parameters for querying items. + * @returns A promise that resolves with the list of threads. + */ + async list( + objectIds: string[], + tokenId?: string | null, + address?: string | null, + { limit = 20, offset = 0, sort = 'created', order = 'desc' }: QueryParams = {} + ) { + await Promise.all(objectIds.map((objectId) => validateObject(this.ain, objectId))); + + if (tokenId) { + await Promise.all(objectIds.map((objectId) => validateToken(this.ain, objectId, tokenId))); + } + + const allThreads = await Promise.all( + objectIds.map(async (objectId) => { + const tokens = await this.fetchTokens(objectId); + return this.flattenThreads(objectId, tokens); + }) + ); + const threads = allThreads.flat(); + + const filtered = this.filterThreads(threads, tokenId, address); + const sorted = this.sortThreads(filtered, sort, order); + + const total = sorted.length; + const items = sorted.slice(offset, offset + limit); + + return { total, items }; + } + + private buildTxBodyForCreateThread( + address: string, + objectId: string, + tokenId: string, + { id, metadata, created_at }: Thread + ) { + const appId = AinftObject.getAppId(objectId); + const threadPath = Path.app(appId).token(tokenId).ai().history(address).thread(id).value(); + const value = { + id, + createdAt: created_at, + messages: true, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + return buildSetTxBody(buildSetValueOp(threadPath, value), address); + } + + private async buildTxBodyForUpdateThread( + address: string, + objectId: string, + tokenId: string, + { id, metadata }: Thread + ) { + const appId = AinftObject.getAppId(objectId); + const threadPath = Path.app(appId).token(tokenId).ai().history(address).thread(id).value(); + const prev = await getValue(this.ain, threadPath); + const value = { + ...prev, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; + return buildSetTxBody(buildSetValueOp(threadPath, value), address); + } + + private buildTxBodyForDeleteThread( + address: string, + objectId: string, + tokenId: string, + threadId: string + ) { + const appId = AinftObject.getAppId(objectId); + const threadPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .value(); + return buildSetTxBody(buildSetValueOp(threadPath, null), address); + } + + private async fetchTokens(objectId: string) { + const appId = AinftObject.getAppId(objectId); + const tokensPath = Path.app(appId).tokens().value(); + return this.ain.db.ref(tokensPath).getValue(); + } + + private flattenThreads(objectId: string, tokens: any) { + const flatten: any = []; + _.forEach(tokens, (token, tokenId) => { + const assistant = token.ai; + if (!assistant) { + return; + } + const histories = assistant.history; + if (typeof histories !== 'object' || histories === true) { + return; + } + _.forEach(histories, (history, address) => { + const threads = _.get(history, 'threads'); + _.forEach(threads, (thread) => { + let updatedAt = thread.createdAt; + if (typeof thread.messages === 'object' && thread.messages !== null) { + const keys = Object.keys(thread.messages); + updatedAt = Number(keys[keys.length - 1]); + } + flatten.push({ + id: thread.id, + metadata: thread.metadata || {}, + created_at: thread.createdAt, + updated_at: updatedAt, + assistant: { + id: assistant.id, + objectId, + tokenId, + model: assistant.config.model, + name: assistant.config.name, + instructions: assistant.config.instructions, + description: assistant.config.description || null, + metadata: assistant.config.metadata || {}, + created_at: assistant.createdAt, + }, + author: { address }, + }); + }); + }); + }); + return flatten; + } + + private filterThreads(threads: any, tokenId?: string | null, address?: string | null) { + return _.filter(threads, (thread) => { + const threadTokenId = _.get(thread, 'assistant.tokenId'); + const threadAddress = _.get(thread, 'author.address'); + const tokenIdMatch = tokenId ? threadTokenId === tokenId : true; + const addressMatch = address ? threadAddress === address : true; + return tokenIdMatch && addressMatch; + }); + } + + private sortThreads(threads: any, sort: string, order: 'asc' | 'desc') { + if (sort === 'created') { + return _.orderBy(threads, ['created_at'], [order]); + } else if (sort === 'updated') { + return _.orderBy(threads, ['updated_at'], [order]); + } else { + throw new AinftError('bad-request', `invalid sort criteria: ${sort}`); + } + } +} diff --git a/src/ainft.ts b/src/ainft.ts index 718663da..bf4d2113 100644 --- a/src/ainft.ts +++ b/src/ainft.ts @@ -1,10 +1,11 @@ import axios from 'axios'; +import _ from 'lodash'; import Ain from '@ainblockchain/ain-js'; import Ainize from '@ainize-team/ainize-js'; import * as AinUtil from '@ainblockchain/ain-util'; import { AinWalletSigner } from '@ainblockchain/ain-js/lib/signer/ain-wallet-signer'; import { Signer } from '@ainblockchain/ain-js/lib/signer/signer'; -import _ from 'lodash'; +import Handler from '@ainize-team/ainize-js/dist/handlers/handler'; import Nft from './nft'; import Credit from './credit'; @@ -16,63 +17,76 @@ import PersonaModels from './personaModels'; import TextToArt from './textToArt'; import Activity from './activity'; import Eth from './eth'; -import Chat from './chat/chat'; +import { Ai, Assistants, Threads, Messages } from './ai'; import { AINFT_SERVER_ENDPOINT, - AIN_BLOCKCHAIN_CHAINID, + AIN_BLOCKCHAIN_CHAIN_ID, AIN_BLOCKCHAIN_ENDPOINT, } from './constants'; +import { setEnv } from './utils/env'; +import { ConnectParams } from './types'; +import { authenticated } from './utils/decorator'; +import { AinftError } from './error'; + +export interface ClientOptions { + privateKey?: string; + signer?: Signer; + baseUrl?: string | null; + blockchainUrl?: string | null; + chainId?: 0 | 1 | null; +} /** * A class that establishes a blockchain and ainft server connection and initializes other classes. */ export default class AinftJs { - private baseUrl: string; - private ainize: Ainize; + public ain: Ain; + public ainize: Ainize; public nft: Nft; public credit: Credit; public auth: Auth; public discord: Discord; public event: Event; public store: Store; - public ain: Ain; public personaModels: PersonaModels; public textToArt: TextToArt; public activity: Activity; public eth: Eth; - public chat: Chat; - - constructor( - privateKey: string, - config?: { - ainftServerEndpoint?: string, - ainBlockchainEndpoint?: string, - chainId?: number, - } - ) { - this.baseUrl = _.get(config, 'ainftServerEndpoint') || AINFT_SERVER_ENDPOINT['prod']; - const stage = this.getStage(this.baseUrl); - const chainId = _.get(config, 'chainId') || AIN_BLOCKCHAIN_CHAINID[stage]; - - if (!(chainId === 0 || chainId === 1)) { - throw new Error(`Invalid chain ID: ${chainId}`); - } - - this.ain = new Ain(_.get(config, 'ainBlockchainEndpoint') || AIN_BLOCKCHAIN_ENDPOINT[stage], null, chainId); - this.ainize = new Ainize(chainId); - this.setPrivateKey(privateKey); - - this.nft = new Nft(this.ain, this.baseUrl, '/nft'); - this.eth = new Eth(this.ain, this.baseUrl, '/nft'); - this.credit = new Credit(this.ain, this.baseUrl, '/credit'); - this.auth = new Auth(this.ain, this.baseUrl, '/auth'); - this.discord = new Discord(this.ain, this.baseUrl, '/discord'); - this.event = new Event(this.ain, this.baseUrl, '/event'); - this.store = new Store(this.ain, this.baseUrl, '/store'); - this.personaModels = new PersonaModels(this.ain, this.baseUrl, '/persona-models'); - this.textToArt = new TextToArt(this.ain, this.baseUrl, '/text-to-art'); - this.activity = new Activity(this.ain, this.baseUrl, '/activity'); - this.chat = new Chat(this.ain, this.ainize); + public ai: Ai; + public assistant: Assistants; + public thread: Threads; + public message: Messages; + + private _baseUrl: string; + private _blockchainUrl: string; + private _chainId: 0 | 1; + + constructor({ privateKey, signer, baseUrl, blockchainUrl, chainId }: ClientOptions = {}) { + this._baseUrl = baseUrl || 'https://ainft-api.ainetwork.ai'; + const stage = this.getStage(this._baseUrl); + this._blockchainUrl = blockchainUrl || AIN_BLOCKCHAIN_ENDPOINT[stage]; + this._chainId = chainId || AIN_BLOCKCHAIN_CHAIN_ID[stage]; + + this.ain = new Ain(this._blockchainUrl, null, this._chainId); + this.ainize = new Ainize(this._chainId); + + this.setCredentials(privateKey, signer); + setEnv(stage); + + this.nft = new Nft(this._baseUrl, '/nft', this.ain); + this.eth = new Eth(this._baseUrl, '/nft', this.ain); + this.credit = new Credit(this._baseUrl, '/credit', this.ain); + this.auth = new Auth(this._baseUrl, '/auth', this.ain, this.ainize); + this.discord = new Discord(this._baseUrl, '/discord', this.ain); + this.event = new Event(this._baseUrl, '/event', this.ain); + this.store = new Store(this._baseUrl, '/store', this.ain); + this.personaModels = new PersonaModels(this._baseUrl, '/persona-models', this.ain); + this.textToArt = new TextToArt(this._baseUrl, '/text-to-art', this.ain); + this.activity = new Activity(this._baseUrl, '/activity', this.ain); + this.ai = new Ai(this._baseUrl, null, this.ain, this.ainize); + this.assistant = new Assistants(this._baseUrl, null, this.ain, this.ainize); + this.thread = new Threads(this._baseUrl, null, this.ain, this.ainize); + this.message = new Messages(this._baseUrl, null, this.ain, this.ainize); } /** @@ -80,7 +94,7 @@ export default class AinftJs { * @param baseUrl */ setBaseUrl(baseUrl: string) { - this.baseUrl = baseUrl; + this._baseUrl = baseUrl; this.nft.setBaseUrl(baseUrl); this.credit.setBaseUrl(baseUrl); this.auth.setBaseUrl(baseUrl); @@ -97,11 +111,7 @@ export default class AinftJs { * @param providerUrl * @param chainId */ - setAiNetworkInfo( - providerUrl: string, - chainId: number, - axiosConfig?: any - ) { + setAiNetworkInfo(providerUrl: string, chainId: number, axiosConfig?: any) { this.ain.setProvider(providerUrl, null, chainId, axiosConfig); } @@ -113,6 +123,14 @@ export default class AinftJs { return this.ain.wallet.defaultAccount; } + setCredentials(privateKey?: string, signer?: Signer) { + if (privateKey) { + this.setPrivateKey(privateKey); + } else if (signer) { + this.setSigner(signer); + } + } + /** * Set a new privateKey. From now on, you can access the apps that match your new privateKey. * @param privateKey @@ -132,12 +150,55 @@ export default class AinftJs { this.setSigner(signer); } + /** + * Connects to the blockchain endpoint. + * @param {ConnectParams} connectParams - The parameters to connect. + */ + @authenticated + async connect({ connectionCb, disconnectionCb, customClientId }: ConnectParams = {}) { + if (this.isConnected()) { + throw new AinftError( + 'already-exists', + 'connection to the blockchain network is already established.' + ); + } + const privateKey = this.ain.wallet.defaultAccount?.private_key; + if (privateKey) { + await this.ainize.login(privateKey, connectionCb, disconnectionCb, customClientId); + } else { + await this.ainize.loginWithSigner(connectionCb, disconnectionCb, customClientId); + } + } + + /** + * Checks whether the client is connected to the blockchain endpoint. + * @returns Returns `true` if connected, `false` otherwise. + */ + @authenticated + isConnected(): boolean { + return Handler.getInstance().isConnected(); + } + + /** + * Disconnects from the blockchain endpoint. + */ + @authenticated + async disconnect() { + if (!this.isConnected()) { + throw new AinftError( + 'unavailable', + 'connection to the blockchain network could not be established.' + ); + } + await this.ainize.logout(); + } + /** * Return the status of the AINFT server. * @returns */ async getStatus(): Promise<{ health: boolean }> { - return (await axios.get(`${this.baseUrl}/status`)).data; + return (await axios.get(`${this._baseUrl}/status`)).data; } static createAccount() { diff --git a/src/ainft721Object.ts b/src/ainft721Object.ts index 8e016186..5744f540 100644 --- a/src/ainft721Object.ts +++ b/src/ainft721Object.ts @@ -1,7 +1,9 @@ -import { AinftToken } from "./ainftToken"; -import FactoryBase from "./factoryBase"; -import { HttpMethod } from "./types"; -import Ain from "@ainblockchain/ain-js"; +import { AinftToken } from './ainftToken'; +import FactoryBase from './factoryBase'; +import { HttpMethod, Metadata } from './types'; +import Ain from '@ainblockchain/ain-js'; +import { authenticated } from './utils/decorator'; +import { AinftError } from './error'; /** * The class of AINFT 721 object. @@ -17,6 +19,10 @@ export default class Ainft721Object extends FactoryBase { readonly owner: string; /** The ID of app in AIN blockchain. */ readonly appId: string; + /** The metadata of AINFT object. */ + readonly metadata?: Metadata; + /** The URL slug of AINFT object. */ + readonly slug?: string | null; /** * Constructor of Ainft721Object. @@ -25,17 +31,32 @@ export default class Ainft721Object extends FactoryBase { * @param objectInfo.id The ID of AINFT object. * @param objectInfo.name The name of AINFT object. * @param objectInfo.symbol The symbol of AINFT object. - * @param objectInfo.owner Owner of AINFT object. + * @param objectInfo.owner The owner of AINFT object. + * @param objectInfo.metadata The metadata of AINFT object. + * @param objectInfo.slug The URL slug of AINFT object. * @param ain Ain instance to sign and send transaction to AIN blockchain. * @param baseUrl The base url to request api to AINFT factory server. */ - constructor(objectInfo: { id: string, name: string, symbol: string, owner: string}, ain: Ain, baseUrl: string) { - super(ain, baseUrl); + constructor( + objectInfo: { + id: string; + name: string; + symbol: string; + owner: string; + metadata?: Metadata; + slug?: string; + }, + ain: Ain, + baseUrl: string + ) { + super(baseUrl, null, ain); this.id = objectInfo.id; this.name = objectInfo.name; this.symbol = objectInfo.symbol; this.owner = objectInfo.owner; this.appId = Ainft721Object.getAppId(objectInfo.id); + this.metadata = objectInfo.metadata || {}; + this.slug = objectInfo.slug || null; } /** @@ -44,7 +65,7 @@ export default class Ainft721Object extends FactoryBase { * @returns Returns AINFT token instance. * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * async function main() { * const ainftObject = await ainftJs.nft.get('YOUR-AINFT-OBJECT-ID'); @@ -53,12 +74,24 @@ export default class Ainft721Object extends FactoryBase { * ``` */ async getToken(tokenId: string): Promise { - const { nfts } = await this.sendRequestWithoutSign(HttpMethod.GET, `native/search/nfts`, { ainftObjectId: this.id, tokenId }); + const { nfts } = await this.sendRequestWithoutSign(HttpMethod.GET, `native/search/nfts`, { + ainftObjectId: this.id, + tokenId, + }); if (nfts.length === 0) { - throw new Error('Token not found'); + throw new AinftError('not-found', `token not found: ${tokenId}`); } const token = nfts[0]; - return new AinftToken({ ainftObjectId: this.id, tokenId, tokenURI: token.tokenURI, metadata: token.metadata }, this.ain, this.baseUrl); + return new AinftToken( + { + ainftObjectId: this.id, + tokenId, + tokenURI: token.tokenURI, + metadata: token.metadata, + }, + this.ain, + this.baseUrl + ); } /** @@ -67,10 +100,10 @@ export default class Ainft721Object extends FactoryBase { * @param to The address the AINFT will be send to. * @param tokenId Token ID of AINFT. * @returns Returns transaction result. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * async function main() { * const ainftObject = await ainftJs.nft.get('YOUR-AINFT-OBJECT-ID'); @@ -79,8 +112,9 @@ export default class Ainft721Object extends FactoryBase { * } * ``` */ + @authenticated async transfer(from: string, to: string, tokenId: string): Promise { - const txbody = await this.getTxBodyForTransfer(from, to, tokenId); + const txbody = await this.getTxBodyForTransfer(from, to, tokenId); return this.ain.sendTransaction(txbody); } @@ -91,7 +125,7 @@ export default class Ainft721Object extends FactoryBase { * @returns Returns transaction result. * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * async function main() { * const ainftObject = await ainftJs.nft.get('YOUR-AINFT-OBJECT-ID'); @@ -100,6 +134,7 @@ export default class Ainft721Object extends FactoryBase { * } * ``` */ + @authenticated async mint(to: string, tokenId: string): Promise { const address = await this.ain.signer.getAddress(); const txbody = await this.getTxBodyForMint(address, to, tokenId); @@ -112,10 +147,10 @@ export default class Ainft721Object extends FactoryBase { * @param to The address the AINFT will be send to. * @param tokenId Token ID of AINFT. * @returns Returns transaction body without signature. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * async function main() { * const ainftObject = await ainftJs.nft.get('YOUR-AINFT-OBJECT-ID'); @@ -124,11 +159,12 @@ export default class Ainft721Object extends FactoryBase { * } * ``` */ + @authenticated getTxBodyForTransfer(from: string, to: string, tokenId: string) { const body = { address: from, toAddress: to, - } + }; const trailingUrl = `native/${this.id}/${tokenId}/transfer`; return this.sendRequest(HttpMethod.POST, trailingUrl, body); } @@ -139,10 +175,10 @@ export default class Ainft721Object extends FactoryBase { * @param to The address the AINFT will be send. * @param tokenId Token ID of AINFT. * @returns Returns transaction body without signature. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * async function main() { * const ainftObject = await ainftJs.nft.get('YOUR-AINFT-OBJECT-ID'); @@ -151,21 +187,22 @@ export default class Ainft721Object extends FactoryBase { * } * ``` */ + @authenticated getTxBodyForMint(ownerAddress: string, to: string, tokenId: string) { const body = { address: ownerAddress, toAddress: to, tokenId, - } + }; const trailingUrl = `native/${this.id}/mint`; return this.sendRequest(HttpMethod.POST, trailingUrl, body); } /** * Gets app ID by AINFT object ID. - * @param id + * @param id */ - static getAppId(id: string): string { - return `ainft721_${id.toLowerCase()}`; + static getAppId(objectId: string): string { + return `ainft721_${objectId.toLowerCase()}`; } -} \ No newline at end of file +} diff --git a/src/ainftToken.ts b/src/ainftToken.ts index 092f7866..11043370 100644 --- a/src/ainftToken.ts +++ b/src/ainftToken.ts @@ -1,6 +1,7 @@ import Ain from "@ainblockchain/ain-js"; import FactoryBase from "./factoryBase"; import { HttpMethod } from "./types"; +import { authenticated } from "./utils/decorator"; /** * The class of AINFT Token. @@ -27,7 +28,7 @@ export class AinftToken extends FactoryBase { * @param baseUrl The base url to request api to AINFT factory server. */ constructor(tokenInfo: { ainftObjectId: string, tokenId: string, tokenURI: string, metadata?: object }, ain: Ain, baseUrl: string) { - super(ain, baseUrl); + super(baseUrl, null, ain); this.ainftObjectId = tokenInfo.ainftObjectId; this.tokenId = tokenInfo.tokenId; this.tokenURI = tokenInfo.tokenURI; @@ -54,6 +55,7 @@ export class AinftToken extends FactoryBase { * } * ``` */ + @authenticated async setMetadata(metadata: object) { const address = await this.ain.signer.getAddress(); const body = { ainftObjectId: this.ainftObjectId, tokenId: this.tokenId, metadata, userAddress: address }; diff --git a/src/auth.ts b/src/auth.ts index a27fe1da..f90f29a7 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -2,6 +2,7 @@ import Reference from '@ainblockchain/ain-js/lib/ain-db/ref'; import FactoryBase from './factoryBase'; import { APP_STAKING_LOCKUP_DURATION_MS, MIN_GAS_PRICE } from './constants'; import { HttpMethod, User } from './types'; +import { authenticated } from './utils/decorator'; /** * This class supports creating AINFT factory app and users. \ @@ -13,6 +14,7 @@ export default class Auth extends FactoryBase { * @param {string} appname - The name of app. * @returns Returns transaction result. */ + @authenticated async createApp(appname: string) { const address = await this.ain.signer.getAddress(); return this.ain.db.ref(`/manage_app/${appname}/create/${Date.now()}`).setValue({ @@ -38,6 +40,7 @@ export default class Auth extends FactoryBase { * @param {string} userId The ID of user to create. * @returns */ + @authenticated createUser(appId: string, userId: string): Promise { const body = { appId, userId }; const trailingUrl = 'create_user_account'; @@ -50,12 +53,13 @@ export default class Auth extends FactoryBase { * @param {string} userId The ID of user to be admin. * @returns */ + // TODO(hyeonwoong): add registerUserToAdmin and deregisterUserFromAdmin + @authenticated createAdmin(appId: string, userId: string): Promise { const body = { appId, userId }; const trailingUrl = 'create_admin_account'; return this.sendRequest(HttpMethod.POST, trailingUrl, body); } - // TODO(hyeonwoong): add registerUserToAdmin and deregisterUserFromAdmin /** * Gets AINFT factory user in app. @@ -63,6 +67,7 @@ export default class Auth extends FactoryBase { * @param {string} userId The ID of user to get. * @returns Return user information. */ + @authenticated getUser(appId: string, userId: string): Promise { const query = { appId }; const trailingUrl = `user/${userId}`; @@ -76,6 +81,7 @@ export default class Auth extends FactoryBase { * @param {string} ethAddress The ethereum address to add. * @returns */ + @authenticated addUserEthAddress(appId: string, userId: string, ethAddress: string): Promise { const body = { appId, @@ -92,6 +98,7 @@ export default class Auth extends FactoryBase { * @param {string} ethAddress The ethereum address to delete. * @returns */ + @authenticated removeUserEthAddress(appId: string, userId: string, ethAddress: string): Promise { const query = { appId, @@ -109,6 +116,7 @@ export default class Auth extends FactoryBase { * @param {string} contractAddress The address of contract. * @returns */ + @authenticated addManagedContract( appId: string, chain: string, @@ -133,6 +141,7 @@ export default class Auth extends FactoryBase { * @param {string} contractAddress The address of contract. * @returns */ + @authenticated removeManagedContract( appId: string, chain: string, @@ -155,6 +164,7 @@ export default class Auth extends FactoryBase { * @param {string} accessAinAddress This is the address of the account that will be accessible to the AINFT Factory app. If not set, it is set to the address of the account set as the privateKey. * @returns */ + @authenticated async registerBlockchainApp(appId: string, accessAinAddress?: string) { const ownerAddress = await this.ain.signer.getAddress(); const body = { @@ -172,6 +182,7 @@ export default class Auth extends FactoryBase { * @param {string} appId The ID of app. * @returns */ + @authenticated async getTxBodyForDelegateApp(appId: string) { const address = await this.ain.signer.getAddress(); const body = { appId, address }; @@ -185,6 +196,7 @@ export default class Auth extends FactoryBase { * @param {string} appId The ID of app. * @returns */ + @authenticated async delegateApp(appId: string) { const txBody = await this.getTxBodyForDelegateApp(appId); return this.ain.sendTransaction(txBody); @@ -197,6 +209,7 @@ export default class Auth extends FactoryBase { * @param {string} userId The ID of user. * @param {string} chain The symbol of chain. */ + @authenticated getUserDepositAddress(appId: string, userId: string, chain: string): Promise { const body = { appId, @@ -212,6 +225,7 @@ export default class Auth extends FactoryBase { * @param {string[]} owners The list of addresses to be owner. * @returns {string} transaction hash */ + @authenticated addOwners(appId: string, owners: string[]): Promise { const body = { appId, diff --git a/src/blockchainBase.ts b/src/blockchainBase.ts deleted file mode 100644 index bf79f068..00000000 --- a/src/blockchainBase.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Ain from '@ainblockchain/ain-js'; -import Ainize from '@ainize-team/ainize-js'; - -export default class BlockchainBase { - protected ain: Ain; - protected ainize: Ainize; - - constructor(ain: Ain, ainize: Ainize) { - this.ain = ain; - this.ainize = ainize; - } -} diff --git a/src/chat/assistants.ts b/src/chat/assistants.ts deleted file mode 100644 index 0ec580b6..00000000 --- a/src/chat/assistants.ts +++ /dev/null @@ -1,316 +0,0 @@ -import Ainft721Object from '../ainft721Object'; -import BlockchainBase from '../blockchainBase'; -import { - Assistant, - AssistantCreateParams, - AssistantDeleteTransactionResult, - AssistantDeleted, - AssistantTransactionResult, - AssistantUpdateParams, - JobType, - ServiceNickname, -} from '../types'; -import { - buildSetTransactionBody, - buildSetValueOp, - isTransactionSuccess, - Ref, - validateAndGetServiceName, - validateAndGetService, - validateAssistant, - validateAssistantNotExists, - validateObject, - validateObjectOwner, - validateServiceConfiguration, - validateToken, - sendAinizeRequest, - sendTransaction, - buildSetWriteRuleOp, - buildSetOp, - buildSetStateRuleOp, -} from '../common/util'; -import { MESSAGE_GC_MAX_SIBLINGS, MESSAGE_GC_NUM_SIBLINGS_DELETED } from '../constants'; - -/** - * This class supports building assistants that enables conversation with LLM models.\ - * Do not create it directly; Get it from AinftJs instance. - */ -export default class Assistants extends BlockchainBase { - /** - * Create an assistant with a model and instructions. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @param {AssistantCreateParams} AssistantCreateParams - The parameters to create assistant. - * @returns Returns a promise that resolves with both the transaction result and the created assistant. - */ - async create( - objectId: string, - tokenId: string, - nickname: ServiceNickname, - { model, name, instructions, description, metadata }: AssistantCreateParams - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateObjectOwner(appId, address, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistantNotExists(appId, tokenId, serviceName, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.CREATE_ASSISTANT; - const body = { - model, - name, - instructions, - ...(description && { description }), - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - const assistant = await sendAinizeRequest( - jobType, - body, - service, - this.ain, - this.ainize - ); - - const txBody = this.buildTxBodyForCreateAssistant( - assistant, - appId, - tokenId, - serviceName, - address - ); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, assistant }; - } - - /** - * Updates an assistant. - * @param {string} assistantId - The ID of assistant. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @param {AssistantUpdateParams} AssistantUpdateParams - The parameters to update assistant. - * @returns Returns a promise that resolves with both the transaction result and the updated assistant. - */ - async update( - assistantId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname, - { model, name, instructions, description, metadata }: AssistantUpdateParams - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateObjectOwner(appId, address, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, assistantId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.MODIFY_ASSISTANT; - const body = { - assistantId, - ...(model && { model }), - ...(name && { name }), - ...(instructions && { instructions }), - ...(description && { description }), - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - const assistant = await sendAinizeRequest( - jobType, - body, - service, - this.ain, - this.ainize - ); - - const txBody = this.buildTxBodyForUpdateAssistant( - assistant, - appId, - tokenId, - serviceName, - address - ); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, assistant }; - } - - /** - * Deletes an assistant. - * @param {string} assistantId - The ID of assistant. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with both the transaction result and the deleted assistant. - */ - async delete( - assistantId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateObjectOwner(appId, address, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, assistantId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.DELETE_ASSISTANT; - const body = { assistantId }; - const delAssistant = await sendAinizeRequest( - jobType, - body, - service, - this.ain, - this.ainize - ); - - const txBody = this.buildTxBodyForDeleteAssistant(appId, tokenId, serviceName, address); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, delAssistant }; - } - - /** - * Retrieves an assistant. - * @param {string} assistantId - The ID of assistant. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with the assistant. - */ - async get( - assistantId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, assistantId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.RETRIEVE_ASSISTANT; - const body = { assistantId }; - const assistant = await sendAinizeRequest( - jobType, - body, - service, - this.ain, - this.ainize - ); - - return assistant; - } - - private buildTxBodyForCreateAssistant( - assistant: Assistant, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const { id, model, name, instructions, description, metadata } = assistant; - const ref = Ref.app(appId).token(tokenId).ai(serviceName).root(); - const path = `/apps/${appId}/tokens/${tokenId}/ai/${serviceName}/history/$user_addr`; - const subpath = 'threads/$thread_id/messages/$message_id'; - - const config = { - model, - name, - instructions, - ...(description && { description }), - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - const value = { id, config, history: true }; - const rule = { - write: `auth.addr === $user_addr || util.isAppAdmin('${appId}', auth.addr, getValue) === true`, - state: { - gc_max_siblings: MESSAGE_GC_MAX_SIBLINGS, - gc_num_siblings_deleted: MESSAGE_GC_NUM_SIBLINGS_DELETED, - }, - }; - - const setValueOp = buildSetValueOp(ref, value); - const setWriteRuleOp = buildSetWriteRuleOp(path, rule.write); - const setGCRuleOp = buildSetStateRuleOp(`${path}/${subpath}`, rule.state); - const setOp = buildSetOp([setValueOp, setWriteRuleOp, setGCRuleOp]); - - return buildSetTransactionBody(setOp, address); - } - - private buildTxBodyForUpdateAssistant( - assistant: Assistant, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const { model, name, instructions, description, metadata } = assistant; - const ref = Ref.app(appId).token(tokenId).ai(serviceName).config(); - - const value = { - model, - name, - instructions, - ...(description && { description }), - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - - return buildSetTransactionBody(buildSetValueOp(ref, value), address); - } - - private buildTxBodyForDeleteAssistant( - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const ref = Ref.app(appId).token(tokenId).ai(serviceName).root(); - const path = `/apps/${appId}/tokens/${tokenId}/ai/${serviceName}/history/$user_addr`; - const subpath = 'threads/$thread_id/messages/$message_id'; - - const unsetGCRuleOp = buildSetStateRuleOp(`${path}/${subpath}`, null); - const unsetWriteRuleOp = buildSetWriteRuleOp(path, null); - const unsetValueOp = buildSetValueOp(ref, null); - const unsetOp = buildSetOp([unsetGCRuleOp, unsetWriteRuleOp, unsetValueOp]); - - return buildSetTransactionBody(unsetOp, address); - } -} diff --git a/src/chat/chat.ts b/src/chat/chat.ts deleted file mode 100644 index 5996021d..00000000 --- a/src/chat/chat.ts +++ /dev/null @@ -1,147 +0,0 @@ -import Ain from '@ainblockchain/ain-js'; -import Ainize from '@ainize-team/ainize-js'; -import Service from '@ainize-team/ainize-js/dist/service'; - -import Ainft721Object from '../ainft721Object'; -import BlockchainBase from '../blockchainBase'; -import Assistants from './assistants'; -import Threads from './threads'; -import Messages from './messages'; -import { - ServiceType, - ServiceNickname, - CreditTransactionResult, - ChatConfigurationTransactionResult, - ChatConfiguration, -} from '../types'; -import { - ainizeLogin, - ainizeLogout, - buildSetTransactionBody, - buildSetValueOp, - isTransactionSuccess, - Ref, - sleep, - validateAndGetServiceName, - validateAndGetService, - validateObject, - validateObjectOwner, - validateService, - sendTransaction, -} from '../common/util'; - -/** - * This class supports configuring chat functionality for an AINFT object,\ - * and managing the required credits for its use.\ - * Do not create it directly; Get it from AinftJs instance. - */ -export default class Chat extends BlockchainBase { - assistant: Assistants; - thread: Threads; - message: Messages; - - constructor(ain: Ain, ainize: Ainize) { - super(ain, ainize); - this.assistant = new Assistants(ain, ainize); - this.thread = new Threads(ain, ainize); - this.message = new Messages(ain, ainize); - } - - /** - * Configures chat for an AINFT object. - * @param {string} objectId - The ID of the AINFT object to configure for chat. - * @param {ServiceNickname} nickname - The service nickname. - * @returns {Promise} Returns a promise that resolves with both the transaction result and the chat configuration. - */ - async configure( - objectId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateObjectOwner(appId, address, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateService(serviceName, this.ainize); - - const config = { - type: ServiceType.CHAT, - name: serviceName, - }; - - const txBody = this.buildTxBodyForConfigureChat(config, appId, serviceName, address); - const result = await sendTransaction(txBody, this.ain); - - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, config }; - } - - /** - * Deposits a credits for a service. - * @param {ServiceNickname} nickname - The service nickname for which credits are deposited. - * @param {number} amount - The amount of credits to deposit. - * @returns {Promise} Returns a promise that resolves with the deposit transaction details (hash, address, and updated credit balance). - */ - async depositCredit(nickname: ServiceNickname, amount: number): Promise { - const address = this.ain.signer.getAddress(); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - const service = await validateAndGetService(serviceName, this.ainize); - - await ainizeLogin(this.ain, this.ainize); - - const currentCredit = await service.getCreditBalance(); - const txHash = await service.chargeCredit(amount); - const updatedCredit = await this.waitForUpdate(currentCredit + amount, 60000, txHash, service); // 1min - - await ainizeLogout(this.ainize); - - return { tx_hash: txHash, address, balance: updatedCredit }; - } - - /** - * Get the current credit for a service. - * @param {ServiceNickname} nickname - The service to check the credit. - * @returns {Promise} Returns a promise that resolves with the current credit balance. - */ - async getCredit(nickname: ServiceNickname): Promise { - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - const service = await validateAndGetService(serviceName, this.ainize); - - await ainizeLogin(this.ain, this.ainize); - - const credit = await service.getCreditBalance(); - - await ainizeLogout(this.ainize); - - return credit; - } - - private async waitForUpdate(expected: number, timeout: number, txHash: string, service: Service) { - const startTime = Date.now(); - while (Date.now() - startTime < timeout) { - const credit = await service.getCreditBalance(); - if (credit === expected) { - return expected; - } - await sleep(1000); // 1sec - } - throw new Error(`Credit update timed out. Please check the transaction on insight: ${txHash}`); - } - - private buildTxBodyForConfigureChat( - config: ChatConfiguration, - appId: string, - serviceName: string, - address: string - ) { - const ref = Ref.app(appId).ai(serviceName); - - return buildSetTransactionBody(buildSetValueOp(ref, config), address); - } -} diff --git a/src/chat/messages.ts b/src/chat/messages.ts deleted file mode 100644 index 964349ac..00000000 --- a/src/chat/messages.ts +++ /dev/null @@ -1,390 +0,0 @@ -import Service from '@ainize-team/ainize-js/dist/service'; - -import Ainft721Object from '../ainft721Object'; -import BlockchainBase from '../blockchainBase'; -import { - JobType, - Message, - MessageCreateParams, - MessagesTransactionResult, - MessageMap, - MessageTransactionResult, - MessageUpdateParams, - Page, - ServiceNickname, -} from '../types'; -import { - ainizeLogin, - ainizeLogout, - buildSetTransactionBody, - buildSetValueOp, - getValue, - isTransactionSuccess, - Ref, - sendAinizeRequest, - sendTransaction, - validateAndGetAssistant, - validateAndGetService, - validateAndGetServiceName, - validateAssistant, - validateMessage, - validateObject, - validateServiceConfiguration, - validateThread, - validateToken, -} from '../common/util'; - -/** - * This class supports create messages within threads.\ - * Do not create it directly; Get it from AinftJs instance. - */ -export default class Messages extends BlockchainBase { - /** - * Create a message. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @param {MessageCreateParams} MessageCreateParams - The parameters to create message. - * @returns Returns a promise that resolves with both the transaction result and a list including the new message. - */ - async create( - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname, - body: MessageCreateParams - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - const { id } = await validateAndGetAssistant(appId, tokenId, serviceName, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const messages = await this.sendMessageAndReply(body, threadId, id, service); - - const txBody = this.buildTxBodyForCreateMessage( - threadId, - messages, - appId, - tokenId, - serviceName, - address - ); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, messages }; - } - - /** - * Updates a thread. - * @param {string} messageId - The ID of message. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with both the transaction result and the updated message. - */ - async update( - messageId: string, - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname, - { metadata }: MessageUpdateParams - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - await validateMessage(appId, tokenId, serviceName, address, threadId, messageId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.MODIFY_MESSAGE; - const body = { - threadId, - messageId, - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - const message = await sendAinizeRequest(jobType, body, service, this.ain, this.ainize); - - const txBody = await this.buildTxBodyForUpdateMessage( - message, - appId, - tokenId, - serviceName, - address - ); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, message }; - } - - /** - * Retrieves a message. - * @param {string} messageId - The ID of message. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with the message. - */ - async get( - messageId: string, - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - await validateMessage(appId, tokenId, serviceName, address, threadId, messageId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.RETRIEVE_MESSAGE; - const body = { jobType, threadId, messageId }; - const message = await sendAinizeRequest(jobType, body, service, this.ain, this.ainize); - - return message; - } - - /** - * Retrieves a list of messages. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with the list of messages. - */ - async list( - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.LIST_MESSAGES; - const body = { threadId }; - const { data } = await sendAinizeRequest>( - jobType, - body, - service, - this.ain, - this.ainize - ); - - return data; - } - - private async sendMessageAndReply( - { role, content, metadata }: MessageCreateParams, - threadId: string, - assistantId: string, - service: Service - ) { - try { - await ainizeLogin(this.ain, this.ainize); - const message = await this.createMessage(threadId, role, content, service, metadata); - const run = await this.createRun(threadId, assistantId, service); - await this.waitForRun(threadId, run.id, service); - // TODO(jiyoung): if 'has_more=true', use cursor to fetch more data. - const list = await this.listMessages(threadId, service); - return list.data; - } catch (error: any) { - throw new Error(error); - } finally { - await ainizeLogout(this.ainize); - } - } - - private async createMessage( - threadId: string, - role: 'user', - content: string, - service: Service, - metadata?: object | null - ) { - const { status, data } = await service.request({ - jobType: JobType.CREATE_MESSAGE, - threadId, - role, - content, - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }); - // TODO(jiyoung): extract failure handling to util function. - if (status === 'FAIL') { - throw new Error(`Failed to create message: ${JSON.stringify(data)}`); - } - return data; - } - - private async createRun(threadId: string, assistantId: string, service: Service) { - const { status, data } = await service.request({ - jobType: JobType.CREATE_RUN, - threadId, - assistantId, - }); - // TODO(jiyoung): extract failure handling to util function. - if (status === 'FAIL') { - throw new Error(`Failed to create message: ${JSON.stringify(data)}`); - } - return data; - } - - private waitForRun(threadId: string, runId: string, service: Service) { - return new Promise((resolve, reject) => { - const interval = setInterval(async () => { - try { - const response = await service.request({ - jobType: JobType.RETRIEVE_RUN, - threadId, - runId, - }); - // TODO(jiyoung): extract failure handling to util function. - if (response.status === 'FAIL') { - clearInterval(interval); - reject(new Error(`Failed to retrieve run: ${JSON.stringify(response.data)}`)); - } - if (response.data.status === 'completed') { - clearInterval(interval); - resolve(); - } - if ( - response.data.status === 'expired' || - response.data.status === 'failed' || - response.data.status === 'cancelled' - ) { - clearInterval(interval); - reject(new Error(`Run ${runId} is ${response.data}`)); - } - } catch (error) { - clearInterval(interval); - reject(error); - } - }, 2000); // 2sec - }); - } - - private async listMessages(threadId: string, service: Service) { - const { status, data } = await service.request({ - jobType: JobType.LIST_MESSAGES, - threadId, - }); - // TODO(jiyoung): extract failure handling to util function. - if (status === 'FAIL') { - throw new Error(`Failed to create message: ${JSON.stringify(data)}`); - } - return data; - } - - private buildTxBodyForCreateMessage( - threadId: string, - messages: MessageMap, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const ref = Ref.app(appId) - .token(tokenId) - .ai(serviceName) - .history(address) - .thread(threadId) - .messages(); - const value: { [key: string]: object } = {}; - - Object.keys(messages).forEach((key) => { - const { id, created_at, role, content, metadata } = messages[key]; - value[`${created_at}`] = { - id, - role, - ...(content && Object.keys(content).length > 0 && { content }), - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - }); - - return buildSetTransactionBody(buildSetValueOp(ref, value), address); - } - - private async buildTxBodyForUpdateMessage( - message: Message, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const { id, thread_id, metadata } = message; - const messagesPath = Ref.app(appId) - .token(tokenId) - .ai(serviceName) - .history(address) - .thread(thread_id) - .messages(); - const messages: MessageMap = await getValue(messagesPath, this.ain); - - // TODO(jiyoung): optimize inefficient loop. - let timestamp: string | undefined; - for (const key in messages) { - if (messages[key].id === id) { - timestamp = key; - break; - } - } - - const ref = Ref.app(appId) - .token(tokenId) - .ai(serviceName) - .history(address) - .thread(thread_id) - .message(timestamp!); - const prev = await getValue(ref, this.ain); - - const value = { - ...prev, - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - - return buildSetTransactionBody(buildSetValueOp(ref, value), address); - } -} diff --git a/src/chat/threads.ts b/src/chat/threads.ts deleted file mode 100644 index 28e4a929..00000000 --- a/src/chat/threads.ts +++ /dev/null @@ -1,255 +0,0 @@ -import Ainft721Object from '../ainft721Object'; -import BlockchainBase from '../blockchainBase'; -import { - JobType, - ServiceNickname, - Thread, - ThreadCreateParams, - ThreadDeleteTransactionResult, - ThreadDeleted, - ThreadTransactionResult, - ThreadUpdateParams, -} from '../types'; -import { - buildSetTransactionBody, - buildSetValueOp, - getValue, - isTransactionSuccess, - Ref, - sendAinizeRequest, - sendTransaction, - validateAndGetService, - validateAndGetServiceName, - validateAssistant, - validateObject, - validateServiceConfiguration, - validateThread, - validateToken, -} from '../common/util'; - -/** - * This class supports create threads that assistant can interact with.\ - * Do not create it directly; Get it from AinftJs instance. - */ -export default class Threads extends BlockchainBase { - /** - * Create a thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @param {ThreadCreateParams} ThreadCreateParams - The parameters to create thread. - * @returns Returns a promise that resolves with both the transaction result and the created thread. - */ - async create( - objectId: string, - tokenId: string, - nickname: ServiceNickname, - { metadata }: ThreadCreateParams - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.CREATE_THREAD; - const body = { ...(metadata && Object.keys(metadata).length > 0 && { metadata }) }; - const thread = await sendAinizeRequest(jobType, body, service, this.ain, this.ainize); - - const txBody = this.buildTxBodyForCreateThread(thread, appId, tokenId, serviceName, address); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, thread }; - } - - /** - * Updates a thread. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @param {ThreadUpdateParams} ThreadUpdateParams - The parameters to update thread. - * @returns Returns a promise that resolves with both the transaction result and the updated thread. - */ - async update( - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname, - { metadata }: ThreadUpdateParams - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.MODIFY_THREAD; - const body = { threadId, ...(metadata && Object.keys(metadata).length > 0 && { metadata }) }; - const thread = await sendAinizeRequest(jobType, body, service, this.ain, this.ainize); - - const txBody = await this.buildTxBodyForUpdateThread( - thread, - appId, - tokenId, - serviceName, - address - ); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, thread }; - } - - /** - * Deletes a thread. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with both the transaction result and the deleted thread. - */ - async delete( - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.DELETE_THREAD; - const body = { threadId }; - const delThread = await sendAinizeRequest( - jobType, - body, - service, - this.ain, - this.ainize - ); - - const txBody = this.buildTxBodyForDeleteThread(threadId, appId, tokenId, serviceName, address); - const result = await sendTransaction(txBody, this.ain); - if (!isTransactionSuccess(result)) { - throw new Error(`Transaction failed: ${JSON.stringify(result)}`); - } - - return { ...result, delThread }; - } - - /** - * Retrieves a thread. - * @param {string} threadId - The ID of thread. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {ServiceNickname} nickname - The service nickname to use. - * @returns Returns a promise that resolves with the thread. - */ - async get( - threadId: string, - objectId: string, - tokenId: string, - nickname: ServiceNickname - ): Promise { - const appId = Ainft721Object.getAppId(objectId); - const address = this.ain.signer.getAddress(); - - await validateObject(appId, this.ain); - await validateToken(appId, tokenId, this.ain); - - const serviceName = await validateAndGetServiceName(nickname, this.ainize); - await validateServiceConfiguration(appId, serviceName, this.ain); - await validateAssistant(appId, tokenId, serviceName, null, this.ain); - await validateThread(appId, tokenId, serviceName, address, threadId, this.ain); - - const service = await validateAndGetService(serviceName, this.ainize); - - const jobType = JobType.RETRIEVE_THREAD; - const body = { threadId }; - const thread = await sendAinizeRequest(jobType, body, service, this.ain, this.ainize); - - return thread; - } - - private buildTxBodyForCreateThread( - thread: Thread, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const { id, metadata } = thread; - const ref = Ref.app(appId).token(tokenId).ai(serviceName).history(address).thread(id).root(); - - const value = { - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - messages: true, - }; - - return buildSetTransactionBody(buildSetValueOp(ref, value), address); - } - - private async buildTxBodyForUpdateThread( - thread: Thread, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const { id, metadata } = thread; - const ref = Ref.app(appId).token(tokenId).ai(serviceName).history(address).thread(id).root(); - const prev = await getValue(ref, this.ain); - - const value = { - ...prev, - ...(metadata && Object.keys(metadata).length > 0 && { metadata }), - }; - - return buildSetTransactionBody(buildSetValueOp(ref, value), address); - } - - private buildTxBodyForDeleteThread( - threadId: string, - appId: string, - tokenId: string, - serviceName: string, - address: string - ) { - const ref = Ref.app(appId) - .token(tokenId) - .ai(serviceName) - .history(address) - .thread(threadId) - .root(); - - return buildSetTransactionBody(buildSetValueOp(ref, null), address); - } -} diff --git a/src/common/ainizeUtil.ts b/src/common/ainizeUtil.ts deleted file mode 100644 index 91e9a6b6..00000000 --- a/src/common/ainizeUtil.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Ain from '@ainblockchain/ain-js'; -import Ainize from '@ainize-team/ainize-js'; - -export default class AinizeAuth { - private static instance: AinizeAuth; - private _isLoggedIn: boolean = false; - - private constructor() {} - - static getInstance() { - if (!this.instance) { - this.instance = new AinizeAuth(); - } - return this.instance; - } - - get isLoggedIn() { - return this._isLoggedIn; - } - - async login(ain: Ain, ainize: Ainize) { - if (!this.isLoggedIn) { - const privateKey = ain.wallet.defaultAccount?.private_key!; - await ainize.login(privateKey); - this._isLoggedIn = true; - } - } - - async logout(ainize: Ainize) { - if (this.isLoggedIn) { - await ainize.logout(); - this._isLoggedIn = false; - } - } -} diff --git a/src/common/util.ts b/src/common/util.ts deleted file mode 100644 index 4f6ca4a4..00000000 --- a/src/common/util.ts +++ /dev/null @@ -1,366 +0,0 @@ -import stringify = require('fast-json-stable-stringify'); -import Ain from '@ainblockchain/ain-js'; -import Ainize from '@ainize-team/ainize-js'; -import { SetOperation, SetMultiOperation, TransactionInput } from '@ainblockchain/ain-js/lib/types'; -import Service from '@ainize-team/ainize-js/dist/service'; - -import AinizeAuth from './ainizeUtil'; -import { SERVICE_NAME_MAP, MIN_GAS_PRICE } from '../constants'; -import { HttpMethod, JobType, MessageMap } from '../types'; - -export const buildData = ( - method: HttpMethod, - path: string, - timestamp: number, - data?: Record -) => { - const _data: any = { - method, - path, - timestamp, - }; - - if (!data || Object.keys(data).length === 0) { - return _data; - } - - if (method === HttpMethod.POST || method === HttpMethod.PUT) { - _data['body'] = stringify(data); - } else { - _data['querystring'] = stringify(data); - } - - return _data; -}; - -export const buildSetTransactionBody = ( - operation: SetOperation | SetMultiOperation, - address: string -): TransactionInput => { - return { - operation: operation, - address, - gas_price: MIN_GAS_PRICE, - nonce: -1, - }; -}; - -export const buildSetValueOp = (ref: string, value: any): SetOperation => ({ - type: 'SET_VALUE', - ref, - value, -}); - -export const buildSetWriteRuleOp = (ref: string, rule: any) => buildSetRuleOp(ref, { write: rule }); - -export const buildSetStateRuleOp = (ref: string, rule: any) => buildSetRuleOp(ref, { state: rule }); - -export const buildSetRuleOp = (ref: string, rule: { write?: any; state?: any }): SetOperation => ({ - type: 'SET_RULE', - ref, - value: { - '.rule': { - write: rule.write, - state: rule.state, - }, - }, -}); - -export const buildSetOp = (opList: any[]): SetMultiOperation => ({ - type: 'SET', - op_list: opList, -}); - -export const sendTransaction = (txBody: any, ain: Ain) => { - return ain.sendTransaction(txBody); -}; - -export const isJoiError = (error: any) => { - return error.response?.data?.isJoiError === true; -}; - -export function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -export function serializeEndpoint(endpoint: string) { - return endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint; -} - -export function isTransactionSuccess(transactionResponse: any) { - const { result } = transactionResponse; - if (result.code && result.code !== 0) { - return false; - } - - if (result.result_list) { - const results = Object.values(result.result_list); - return results.every((_result: any) => _result.code === 0); - } - - return true; -} - -export const Ref = { - app: (appId: string): AppRef => { - return { - root: () => `/apps/${appId}`, - ai: (aiName: string) => `${Ref.app(appId).root()}/ai/${aiName}`, - token: (tokenId: string): TokenRef => { - return { - root: () => `${Ref.app(appId).root()}/tokens/${tokenId}`, - ai: (aiName: string): TokenAiRef => { - return { - root: () => `${Ref.app(appId).token(tokenId).root()}/ai/${aiName}`, - config: () => `${Ref.app(appId).token(tokenId).ai(aiName).root()}/config`, - history: (address: string): HistoryRef => { - return { - root: () => - `${Ref.app(appId).token(tokenId).ai(aiName).root()}/history/${address}`, - thread: (threadId: string): ThreadRef => { - return { - root: () => - `${Ref.app(appId) - .token(tokenId) - .ai(aiName) - .history(address) - .root()}/threads/${threadId}`, - messages: () => - `${Ref.app(appId) - .token(tokenId) - .ai(aiName) - .history(address) - .root()}/threads/${threadId}/messages`, - message: (messageId: string) => - `${Ref.app(appId) - .token(tokenId) - .ai(aiName) - .history(address) - .thread(threadId) - .root()}/messages/${messageId}`, - }; - }, - }; - }, - }; - }, - }; - }, - }; - }, -}; - -type AppRef = { - root: () => string; - ai: (aiName: string) => string; - token: (tokenId: string) => TokenRef; -}; - -type TokenRef = { - root: () => string; - ai: (aiName: string) => TokenAiRef; -}; - -type TokenAiRef = { - root: () => string; - config: () => string; - history: (address: string) => HistoryRef; -}; - -type HistoryRef = { - root: () => string; - thread: (threadId: string) => ThreadRef; -}; - -type ThreadRef = { - root: () => string; - messages: () => string; - message: (messageId: string) => string; -}; - -export const validateObject = async (appId: string, ain: Ain) => { - const appPath = Ref.app(appId).root(); - if (!(await exists(appPath, ain))) { - throw new Error('AINFT object not found'); - } -}; - -export const validateObjectOwner = async (appId: string, address: string, ain: Ain) => { - const appPath = Ref.app(appId).root(); - const app = await getValue(appPath, ain); - if (address !== app.owner) { - throw new Error(`${address} is not AINFT object owner`); - } -}; - -export const validateAndGetServiceName = async (name: string, ainize: Ainize): Promise => { - const serviceName = SERVICE_NAME_MAP.get(name); - if (serviceName) return serviceName; - const service = await ainize.getService(name); - if (!service) throw new Error(`Unknown service name: ${name}`); - return name; -}; - -export const validateService = async (serviceName: string, ainize: Ainize): Promise => { - await validateAndGetService(serviceName, ainize); -}; - -export const validateAndGetService = async ( - serviceName: string, - ainize: Ainize -): Promise => { - const service = await ainize.getService(serviceName); - if (!service.isRunning()) { - throw new Error('Service is currently not running'); - } - return service; -}; - -export const validateServiceConfiguration = async ( - appId: string, - serviceName: string, - ain: Ain -) => { - const aiPath = Ref.app(appId).ai(serviceName); - if (!(await exists(aiPath, ain))) { - throw new Error('Service configuration not found. Please call `ainft.chat.configure()` first.'); - } -}; - -export const validateAssistant = async ( - appId: string, - tokenId: string, - serviceName: string, - assistantId: string | null, - ain: Ain -) => { - const assistant = await validateAndGetAssistant(appId, tokenId, serviceName, ain); - if (assistantId && assistantId !== assistant.id) { - throw new Error(`Incorrect assistant ID`); - } -}; - -export const validateAndGetAssistant = async ( - appId: string, - tokenId: string, - serviceName: string, - ain: Ain -) => { - const assistantPath = Ref.app(appId).token(tokenId).ai(serviceName).root(); - const assistant = await getValue(assistantPath, ain); - if (!assistant) { - throw new Error('Assistant not found'); - } - return assistant; -}; - -export const validateAssistantNotExists = async ( - appId: string, - tokenId: string, - serviceName: string, - ain: Ain -) => { - const assistantPath = Ref.app(appId).token(tokenId).ai(serviceName).root(); - const exists = await getValue(assistantPath, ain); - if (exists) { - throw new Error('Assistant already exists'); - } -}; - -export const validateToken = async (appId: string, tokenId: string, ain: Ain) => { - const tokenPath = Ref.app(appId).token(tokenId).root(); - if (!(await exists(tokenPath, ain))) { - throw new Error('Token not found'); - } -}; - -export const validateThread = async ( - appId: string, - tokenId: string, - serviceName: string, - address: string, - threadId: string, - ain: Ain -) => { - const threadPath = Ref.app(appId) - .token(tokenId) - .ai(serviceName) - .history(address) - .thread(threadId) - .root(); - - if (!(await exists(threadPath, ain))) { - throw new Error('Thread not found'); - } -}; - -export const validateMessage = async ( - appId: string, - tokenId: string, - serviceName: string, - address: string, - threadId: string, - messageId: string, - ain: Ain -) => { - const messagesPath = Ref.app(appId) - .token(tokenId) - .ai(serviceName) - .history(address) - .thread(threadId) - .messages(); - const messages: MessageMap = await getValue(messagesPath, ain); - // TODO(jiyoung): optimize inefficient loop. - for (const key in messages) { - if (messages[key].id === messageId) { - return; - } - } - throw new Error('Message not found'); -}; - -export const exists = async (path: string, ain: Ain): Promise => { - return !!(await ain.db.ref(path).getValue()); -}; - -export const getValue = async (path: string, ain: Ain): Promise => { - return ain.db.ref(path).getValue(); -}; - -export const ainizeLogin = async (ain: Ain, ainize: Ainize) => { - return AinizeAuth.getInstance().login(ain, ainize); -}; - -export const ainizeLogout = async (ainize: Ainize) => { - return AinizeAuth.getInstance().logout(ainize); -}; - -export const sendAinizeRequest = async ( - jobType: JobType, - body: object, - service: Service, - ain: Ain, - ainize: Ainize -): Promise => { - let timeout: NodeJS.Timeout | null = null; - try { - await ainizeLogin(ain, ainize); - const timeoutPromise = new Promise( - (_, reject) => (timeout = setTimeout(() => reject(new Error('timeout')), 60000)) // 1min - ); - const response = await Promise.race([service.request({ ...body, jobType }), timeoutPromise]); - if (response.status === 'FAIL') { - throw new Error(JSON.stringify(response.data)); - } - return response.data as T; - } catch (error: any) { - throw new Error(`Ainize service request failed: ${error.message}`); - } finally { - if (timeout) { - clearTimeout(timeout); - } - await ainizeLogout(ainize); - } -}; diff --git a/src/constants.ts b/src/constants.ts index 443c59b3..d5c736ad 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,29 +3,31 @@ export const NODE_ENV = process.env.NODE_ENV; export const AINFT_SERVER_ENDPOINT = { dev: 'https://ainft-api-dev.ainetwork.ai', prod: 'https://ainft-api.ainetwork.ai', -} +}; export const AIN_BLOCKCHAIN_ENDPOINT = { dev: 'https://testnet-api.ainetwork.ai', prod: 'https://mainnet-api.ainetwork.ai', -} +}; - -export const AIN_BLOCKCHAIN_CHAINID = { - dev: 0, - prod: 1 -} +export const AIN_BLOCKCHAIN_CHAIN_ID = { dev: 0, prod: 1 } as const; export const MIN_GAS_PRICE = 500; -export const APP_STAKING_LOCKUP_DURATION_MS = 30 * 1000 // 30 seconds +export const APP_STAKING_LOCKUP_DURATION_MS = 30 * 1000; // 30sec +export const TX_BYTES_LIMIT = 100000; // 100kb export const SUPPORTED_AINFT_STANDARDS = { 721: '721', -} - -export const SERVICE_NAME_MAP = new Map([ - ['openai', 'ainize_openai'] -]); +}; +export const THREAD_GC_MAX_SIBLINGS = 20; +export const THREAD_GC_NUM_SIBLINGS_DELETED = 10; export const MESSAGE_GC_MAX_SIBLINGS = 15; -export const MESSAGE_GC_NUM_SIBLINGS_DELETED = 10; \ No newline at end of file +export const MESSAGE_GC_NUM_SIBLINGS_DELETED = 10; + +export const DEFAULT_AINIZE_SERVICE_NAME = 'aina_backend'; + +export const WHITELISTED_OBJECT_IDS: Record = { + dev: ['0xCE3c4D8dA38c77dEC4ca99cD26B1C4BF116FC401'], + prod: ['0x6C8bB2aCBab0D807D74eB04034aA9Fd8c8E9C365'], +}; diff --git a/src/credit.ts b/src/credit.ts index 6ea2777e..0aa310df 100644 --- a/src/credit.ts +++ b/src/credit.ts @@ -9,6 +9,7 @@ import { LockupList, DepositHistory, } from './types'; +import { authenticated } from './utils/decorator'; // TODO(kriii): Objectify params? /** @@ -24,6 +25,7 @@ export default class Credit extends FactoryBase { * @param {number} maxSupply Maximum number of credits that can be generated. * @returns */ + @authenticated createAppCredit( appId: string, symbol: string, @@ -46,6 +48,7 @@ export default class Credit extends FactoryBase { * @param {string} symbol The symbol of credit. * @returns Returns credit information. */ + @authenticated getAppCredit(appId: string, symbol: string): Promise { const query = { appId, @@ -59,6 +62,7 @@ export default class Credit extends FactoryBase { * @param {string} appId The ID of app. * @param {string} symbol The symbol of credit. */ + @authenticated deleteAppCredit(appId: string, symbol: string): Promise { const query = { appId, @@ -76,6 +80,7 @@ export default class Credit extends FactoryBase { * @param {object} payload The additional data about minting. * @returns */ + @authenticated mintAppCredit( appId: string, symbol: string, @@ -102,6 +107,7 @@ export default class Credit extends FactoryBase { * @param {object} payload The additional data about burning. * @returns */ + @authenticated burnAppCredit( appId: string, symbol: string, @@ -130,6 +136,7 @@ export default class Credit extends FactoryBase { * @param {object} payload The additional data about transferring. * @returns */ + @authenticated transferAppCredit( appId: string, symbol: string, @@ -158,6 +165,7 @@ export default class Credit extends FactoryBase { * @param {number} amount The amount of withdraw credit. * @param {string} userAddress The address where will receive withdraw credit. */ + @authenticated withdrawAppCredit( appId: string, symbol: string, @@ -183,6 +191,7 @@ export default class Credit extends FactoryBase { * @param {string} symbol The symbol of credit. * @return {Promise} Return AppWithdrawList Object */ + @authenticated getWithdrawList(appId: string, symbol: string): Promise { const query = { appId }; const trailingUrl = `symbol/${symbol}/withdraw`; @@ -196,6 +205,7 @@ export default class Credit extends FactoryBase { * @param {string} userId The ID of user. * @returns {Promise} Return UserWithdrawList Object */ + @authenticated getWithdrawListByUserId( appId: string, symbol: string, @@ -213,6 +223,7 @@ export default class Credit extends FactoryBase { * @param {string} userId The ID of user. * @returns {Promise} A Promise that resolves to the credit balance of the user */ + @authenticated getCreditBalanceOfUser( appId: string, symbol: string, @@ -229,6 +240,7 @@ export default class Credit extends FactoryBase { * @param {string} symbol The symbol of credit. * @returns {Promise<{[userId: string]: number}>} A Promise that resolves to the credit balance of all users. */ + @authenticated getCreditBalances( appId: string, symbol: string, @@ -245,6 +257,7 @@ export default class Credit extends FactoryBase { * @param {WithdrawRequestMap} requestMap A map containing withdrawal request information for each user. * @param {string} txHash Hash of transfer transaction. */ + @authenticated withdrawComplete( appId: string, symbol: string, @@ -263,6 +276,7 @@ export default class Credit extends FactoryBase { * @param {WithdrawRequestMap} requestMap A map containing withdrawal request information to reject. * @param {string} reason The reason for the reject. */ + @authenticated rejectWithdrawal( appId: string, symbol: string, @@ -283,6 +297,7 @@ export default class Credit extends FactoryBase { * @param {number} endAt The timestamp when the lockup period ends. * @param {string} reason The reason for the lockup. */ + @authenticated lockupUserBalance( appId: string, symbol: string, @@ -309,6 +324,7 @@ export default class Credit extends FactoryBase { * @param {string} userId The ID of user. * @returns Returns lockup list by userId. */ + @authenticated getUserLockupList( appId: string, symbol: string, @@ -326,6 +342,7 @@ export default class Credit extends FactoryBase { * @param {string} appId The ID of app. * @param {DepositTransaction} transaction The transaction information about deposit. */ + @authenticated depositToken(appId: string, transaction: DepositTransaction): Promise { const body = { appId, transaction }; const trailingUrl = `deposit/transaction`; @@ -339,6 +356,7 @@ export default class Credit extends FactoryBase { * @param {string} chain The symbol of chain. * @returns {Promise} Returns depositHistory list of user. */ + @authenticated getDepositHistory( appId: string, userId: string, diff --git a/src/discord.ts b/src/discord.ts index d2c651f4..54b56517 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -7,6 +7,7 @@ import { InviteInfo, } from "./types"; import FactoryBase from "./factoryBase"; +import { authenticated } from "./utils/decorator"; /** * This class supports the functionality of the AINFT factory for seamless use on Discord.\ @@ -19,6 +20,7 @@ export default class Discord extends FactoryBase { * @param {string} discordGuildId The discord guild ID to which app will be linked. * @returns */ + @authenticated connectDiscordWithApp(appId: string, discordGuildId: string): Promise { const body = { appId, discordServerId: discordGuildId }; const trailingUrl = 'register'; @@ -31,6 +33,7 @@ export default class Discord extends FactoryBase { * @param {string} appId The ID of app. * @returns Returns connected app Id or null. */ + @authenticated getConnectedApp( discordGuildId: string, appId: string = '' @@ -46,6 +49,7 @@ export default class Discord extends FactoryBase { * @param {string} discordGuildId The ID of discord guild. * @returns Returns event list connected discord guild. */ + @authenticated getConnectedEventsByServer( appId: string, discordGuildId: string @@ -62,6 +66,7 @@ export default class Discord extends FactoryBase { * @param {string} discordChannelId The ID of discord channel. * @returns Returns a map of task IDs for each event ID. */ + @authenticated getConnectedTasksByChannel( appId: string, discordGuildId: string, @@ -80,6 +85,7 @@ export default class Discord extends FactoryBase { * @param {string} discordGuildId The discord guild ID to which the model will be linked. * @param {string} discordChannelId The discord channel ID to which the model will be linked. */ + @authenticated addPersonaModelToDiscordChannel( appId: string, modelId: string, @@ -105,6 +111,7 @@ export default class Discord extends FactoryBase { * @param {string} discordChannelId The discord channel ID to which model was connected. * @returns */ + @authenticated getPersonaModelForDiscordChannel( appId: string, discordGuildId: string, @@ -121,6 +128,7 @@ export default class Discord extends FactoryBase { * @param {string} discordGuildId The discord build ID to which model was connected. * @returns Returns a Map of information about the models linked to each channel in the guild." */ + @authenticated getPersonaModelForDiscordServer( appId: string, discordGuildId: string @@ -136,6 +144,7 @@ export default class Discord extends FactoryBase { * @param {string} discordGuildId The discord guild ID to which the model will be disconnected. * @param {string} discordChannelId The discord channel ID to which the model will be disconnected. */ + @authenticated deletePersonaModelFromDiscordChannel( appId: string, discordGuildId: string, @@ -156,6 +165,7 @@ export default class Discord extends FactoryBase { * @param {Array} ambiguousInviters Array of userIds invited in a short time. * @returns {Promise} Return InviteInfo object. */ + @authenticated addInviteInfo( appId: string, eventId: string, @@ -185,6 +195,7 @@ export default class Discord extends FactoryBase { * @param {string} inviteeId The ID of invitee. * @returns {Promise} Return InviteInfo object. */ + @authenticated getInviteInfo( appId: string, discordGuildId: string, diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 00000000..28eba885 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,11 @@ +import { ErrorCode } from './types'; + +export class AinftError extends Error { + readonly code: ErrorCode; + constructor(code: ErrorCode, message: string) { + super(message); + this.name = this.constructor.name; + this.code = code; + this.message = message; + } +} diff --git a/src/eth.ts b/src/eth.ts index fbe5fd73..83032a42 100644 --- a/src/eth.ts +++ b/src/eth.ts @@ -17,6 +17,7 @@ import { RemoveNftSymbolParams, SetEthNftMetadataParams } from "./types"; +import { authenticated } from "./utils/decorator"; /** * This class allows app to register and manage ETH Contracts. This allows you to enrich tokenomics.\ @@ -30,6 +31,7 @@ export default class Eth extends FactoryBase { * @param {AddNftSymbolParams} AddNftSymbolParams The parameters to add NFT symbol. * @returns */ + @authenticated addNftSymbol({ appId, network, @@ -46,6 +48,7 @@ export default class Eth extends FactoryBase { * @param {GetAppNftSymbolListParams} GetAppNftSymbolListParams The parameters to get NFT symbol list in app. * @returns Returns a list of symbols registered in the app. */ + @authenticated getAppNftSymbolList({ appId }: GetAppNftSymbolListParams): Promise { const query = { appId }; const trailingUrl = 'symbol'; @@ -57,6 +60,7 @@ export default class Eth extends FactoryBase { * @param {RemoveNftSymbolParams} RemoveNftSymbolParams The parameters to remove NFT symbol from app. * @returns Returns removed contract information. */ + @authenticated removeNftSymbol({ appId, symbol, @@ -71,6 +75,7 @@ export default class Eth extends FactoryBase { * @param {GetNftSymbolParams} GetNftSymbolParams The parameters to get contract by symbol. * @returns Returns contract information by symbol. */ + @authenticated getContractInfoBySymbol({ appId, symbol, @@ -86,6 +91,7 @@ export default class Eth extends FactoryBase { * @param {GetNftParams} GetNftParams The parameters to get NFT information. * @returns Returns NFT information. */ + @authenticated getNft({ appId, network, @@ -103,6 +109,7 @@ export default class Eth extends FactoryBase { * @param {GetNftContractInfoParams} GetNftContractInfoParams The parameters to get contract information. * @returns Returns contract information. */ + @authenticated getNftContractInfo({ appId, network, @@ -118,6 +125,7 @@ export default class Eth extends FactoryBase { * @param {GetNftsInCollectionParams} GetNftsInCollectionParams * @returns Returns a map of NFTs distinguished by their token IDs. */ + @authenticated getNftsInCollection({ network, collectionId, @@ -135,6 +143,7 @@ export default class Eth extends FactoryBase { * @param {GetUserNftListParams} GetUserNftListParams The parameters to get NFT list user owned. * @returns Returns NFTs owned by the user along with their contract information. */ + @authenticated getUserNftList({ appId, network, @@ -156,6 +165,7 @@ export default class Eth extends FactoryBase { * @param {SetEthNftMetadataParams} SetNftMetadataParams The parameters to set NFT metadata. * @returns Returns set metadata. */ + @authenticated async setNftMetadata({ appId, network, diff --git a/src/event.ts b/src/event.ts index efc3275a..97e18072 100644 --- a/src/event.ts +++ b/src/event.ts @@ -14,6 +14,7 @@ import { History, RewardInfo, } from './types'; +import { authenticated } from './utils/decorator'; /** * This class supports event functionality for activating tokenomics in the community.\ @@ -24,6 +25,7 @@ export default class Event extends FactoryBase { * Creates a new event. Sets the tasks to be performed and the rewards to receive. * @param {CreateEventParams} CreateEventParams The parameters to create event. */ + @authenticated create({ appId, eventId, @@ -52,6 +54,7 @@ export default class Event extends FactoryBase { * Updates an event. * @param {Partial} UpdateEventParams The parameters to update event. */ + @authenticated update({ appId, eventId, @@ -78,6 +81,7 @@ export default class Event extends FactoryBase { * @param {string} appId The ID of app. * @param {string} eventId The ID of event. */ + @authenticated delete(appId: string, eventId: string): Promise { const query = { appId }; const trailingUrl = `${eventId}`; @@ -90,6 +94,7 @@ export default class Event extends FactoryBase { * @param {string} eventId The ID of event. * @returns Returns event information. */ + @authenticated get(appId: string, eventId: string): Promise { const query = { appId }; const trailingUrl = `${eventId}`; @@ -102,6 +107,7 @@ export default class Event extends FactoryBase { * @param {AddEventActivityParams} AddEventActivityParams The parameters to add event activity. * @returns Returns activity ID. */ + @authenticated addActivity({ appId, userId, @@ -124,6 +130,7 @@ export default class Event extends FactoryBase { * @param {GetEventActivityParams} GetEventActivityParams The parameters to get event activity. * @returns {Promise} Returns activity object or null. */ + @authenticated getActivity({ appId, userId, @@ -147,6 +154,7 @@ export default class Event extends FactoryBase { * Updates the activity's status. Activity status includes CREATED, REWARDED, and FAILED. * @param {UpdateEventActivityStatusParams} UpdateEventActivityStatusParams The parameters to update activity status */ + @authenticated updateActivityStatus({ eventId, activityId, @@ -167,6 +175,7 @@ export default class Event extends FactoryBase { * Returns a list of TaskTypes to use for events. * @param {string} appId The ID of app. */ + @authenticated getTaskTypeList(appId: string): Promise { const query = { appId }; const trailingUrl = 'task-types'; @@ -177,6 +186,7 @@ export default class Event extends FactoryBase { * Returns a list of RewardTypes to use for events. * @param {string} appId The ID of app. */ + @authenticated getRewardTypeList(appId: string): Promise { const query = { appId }; const trailingUrl = 'reward-types'; @@ -189,6 +199,7 @@ export default class Event extends FactoryBase { * @param {string} userId The ID of the user who wants to check pending rewards. * @param {string} eventId The ID of event to check pending rewards. */ + @authenticated getPendingRewards( appId: string, userId: string, @@ -206,6 +217,7 @@ export default class Event extends FactoryBase { * @param {string} eventId The ID of the event that the user participated in. * @param {RewardOptions} options The options of reward. */ + @authenticated reward( appId: string, userId: string, @@ -227,6 +239,7 @@ export default class Event extends FactoryBase { * @param {string} userId The ID of the user who wants to check reward history. * @param {string} eventId The ID of event to check reward history. */ + @authenticated getRewardHistory( appId: string, userId: string, @@ -246,6 +259,7 @@ export default class Event extends FactoryBase { * @param {string} userId The ID of the user who wants to check the activity history. * @param {string} eventId The ID of the event you want to check activity history. */ + @authenticated getActivityHistory( appId: string, userId: string, diff --git a/src/factoryBase.ts b/src/factoryBase.ts index dbca939e..e0177e25 100644 --- a/src/factoryBase.ts +++ b/src/factoryBase.ts @@ -1,8 +1,9 @@ import Ain from "@ainblockchain/ain-js"; +import Ainize from '@ainize-team/ainize-js'; import stringify = require("fast-json-stable-stringify"); import axios, { AxiosRequestHeaders } from "axios"; import { HttpMethod, HttpMethodToAxiosMethod, SerializedMessage } from "./types"; -import { buildData, isJoiError, sleep } from "./common/util"; +import { buildData, isJoiError } from './utils/util'; import FormData from "form-data"; /** @@ -15,14 +16,18 @@ export default class FactoryBase { public route: string; /** The Ain object for sign and send transaction to AIN blockchain. */ public ain: Ain; + /** The Ainize object for send request to AIN blockchain. */ + public ainize?: Ainize; constructor( - ain: Ain, baseUrl: string, - route?: string, + route: string | null, + ain: Ain, + ainize?: Ainize, ) { this.route = route || ''; this.ain = ain; + this.ainize = ainize; this.setBaseUrl(baseUrl); } diff --git a/src/nft.ts b/src/nft.ts index b12aa53d..5be0e55b 100644 --- a/src/nft.ts +++ b/src/nft.ts @@ -1,3 +1,5 @@ +import _ from 'lodash'; + import FactoryBase from './factoryBase'; import { DeleteAssetParams, @@ -7,10 +9,13 @@ import { UploadAssetFromDataUrlParams, AinftTokenSearchResponse, AinftObjectSearchResponse, + AinftObjectCreateParams, } from './types'; import Ainft721Object from './ainft721Object'; import stringify from 'fast-json-stable-stringify'; -import { isTransactionSuccess } from './common/util'; +import { isTxSuccess } from './utils/transaction'; +import { authenticated } from './utils/decorator'; +import { AinftError } from './error'; /** * This class supports creating AINFT object, searching AINFTs and things about NFTs.\ @@ -24,7 +29,7 @@ export default class Nft extends FactoryBase { * @returns Transaction hash and AINFT object instance. * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * ainftJs.nft.create('nameOfAinftObject', 'symbolOfAinftObject') * .then((res) => { @@ -38,21 +43,31 @@ export default class Nft extends FactoryBase { * }); * ``` */ - async create(name: string, symbol: string): Promise<{ txHash: string, ainftObject: Ainft721Object }> { + @authenticated + async create({ + name, + symbol, + metadata, + }: AinftObjectCreateParams): Promise<{ txHash: string; ainftObject: Ainft721Object }> { const address = await this.ain.signer.getAddress(); - const body = { address, name, symbol }; + const body = { + address, + name, + symbol, + ...(metadata && !_.isEmpty(metadata) && { metadata }), + }; const trailingUrl = 'native'; const { ainftObjectId, txBody } = await this.sendRequest(HttpMethod.POST, trailingUrl, body); const res = await this.ain.sendTransaction(txBody); - if (!isTransactionSuccess(res)) { - throw Error(`App creation is failed. - ${JSON.stringify(res)}`); + if (!isTxSuccess(res)) { + throw new AinftError('internal', `app creation is failed: ${JSON.stringify(res)}`); } await this.register(ainftObjectId); const ainftObject = await this.get(ainftObjectId); - return { txHash: res.tx_hash, ainftObject } + return { txHash: res.tx_hash, ainftObject }; } /** @@ -61,7 +76,7 @@ export default class Nft extends FactoryBase { * @param ainftObjectId The ID of AINFT object. * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * ainftJs.nft.register('YOUR-AINFT-OBJECT-ID') * .catch((error) => { @@ -69,6 +84,7 @@ export default class Nft extends FactoryBase { * }) * ``` */ + @authenticated async register(ainftObjectId: string): Promise { const address = await this.ain.signer.getAddress(); const message = stringify({ @@ -86,10 +102,10 @@ export default class Nft extends FactoryBase { * Get AINFT object instance by ainftObjectId. * @param ainftObjectId The ID of AINFT object. * @returns Returns the AINFT object corresponding to the given ID. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * ainftJs.nft.get('YOUR-AINFT-OBJECT-ID') * .then((res) => { @@ -104,7 +120,7 @@ export default class Nft extends FactoryBase { async get(ainftObjectId: string): Promise { const { ainftObjects } = await this.searchAinftObjects({ ainftObjectId }); if (ainftObjects.length === 0) { - throw new Error('AINFT object not found'); + throw new AinftError('not-found', `object not found: ${ainftObjectId}`); } const ainftObject = ainftObjects[0]; return new Ainft721Object(ainftObject, this.ain, this.baseUrl); @@ -116,10 +132,10 @@ export default class Nft extends FactoryBase { * @param limit - Sets the maximum number of NFTs to retrieve. * @param cursor - Optional cursor to use for pagination. * @returns Returns AINFTs. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * ainftJs.nft.getAinftsByAinftObject('YOUR-AINFT-OBJECT-ID', 5) * .then((res) => { @@ -131,7 +147,11 @@ export default class Nft extends FactoryBase { * }) * ``` */ - async getAinftsByAinftObject(ainftObjectId: string, limit?: number, cursor?: string): Promise { + async getAinftsByAinftObject( + ainftObjectId: string, + limit?: number, + cursor?: string + ): Promise { return this.searchNfts({ ainftObjectId, limit, cursor }); } @@ -141,10 +161,10 @@ export default class Nft extends FactoryBase { * @param limit - Sets the maximum number of NFTs to retrieve. * @param cursor - Optional cursor to use for pagination. * @returns Returns AINFTs. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * + * * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); * ainftJs.nft.getAinftsByAccount('TOKEN-OWNER-ADDRESS') * .then((res) => { @@ -156,23 +176,29 @@ export default class Nft extends FactoryBase { * }) * ``` */ - async getAinftsByAccount(address: string, limit?: number, cursor?: string): Promise { + async getAinftsByAccount( + address: string, + limit?: number, + cursor?: string + ): Promise { return this.searchNfts({ userAddress: address, limit, cursor }); } /** * Searches for AINFT objects created on the AIN Blockchain. + * This method accesses public data only and does not require signature in the requests. * @param {NftSearchParams} searchParams The parameters to search AINFT object. * @returns Returns searched AINFT objects. - * + * * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * - * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); + * + * const ainftJs = new AinftJs(); * const params = { * userAddress: '0x...', * name: '...', * symbol: '...', + * slug: '...', * limit: 5, * cursor: '...' * } @@ -189,21 +215,22 @@ export default class Nft extends FactoryBase { searchAinftObjects(searchParams: NftSearchParams): Promise { let query: Record = {}; if (searchParams) { - const { userAddress, ainftObjectId, name, symbol, limit, cursor } = searchParams; - query = { userAddress, ainftObjectId, name, symbol, cursor, limit }; + const { userAddress, ainftObjectId, name, symbol, slug, limit, cursor, order } = searchParams; + query = { userAddress, ainftObjectId, name, symbol, slug, cursor, limit, order }; } - const trailingUrl = `native/search/ainftObjects`; - return this.sendRequest(HttpMethod.GET, trailingUrl, query); + const trailingUrl = 'native/search/ainftObjects'; + return this.sendRequestWithoutSign(HttpMethod.GET, trailingUrl, query); } /** * Searches for AINFTs on the ain blockchain. + * This method accesses public data only and does not require signature in the requests. * @param {NftSearchParams} searchParams The parameters to search AINFT. * @returns Returns searched AINFTs * ```ts * import AinftJs from '@ainft-team/ainft-js'; - * - * const ainftJs = new AinftJs('YOUR-PRIVATE-KEY'); + * + * const ainftJs = new AinftJs(); * const params = { * userAddress: '0x...', * name: '...', @@ -225,49 +252,53 @@ export default class Nft extends FactoryBase { searchNfts(searchParams: NftSearchParams): Promise { let query: Record = {}; if (searchParams) { - const { userAddress, ainftObjectId, name, symbol, limit, cursor, tokenId } = searchParams; - query = { userAddress, ainftObjectId, name, symbol, cursor, limit, tokenId }; + const { userAddress, ainftObjectId, name, symbol, slug, tokenId, limit, cursor, order } = + searchParams; + query = { userAddress, ainftObjectId, name, symbol, slug, tokenId, limit, cursor, order }; } - const trailingUrl = `native/search/nfts`; - return this.sendRequest(HttpMethod.GET, trailingUrl, query); + const trailingUrl = 'native/search/nfts'; + return this.sendRequestWithoutSign(HttpMethod.GET, trailingUrl, query); } /** * Upload the asset file using the buffer. - * @param {UploadAssetFromBufferParams} UploadAssetFromBufferParams + * @param {UploadAssetFromBufferParams} UploadAssetFromBufferParams * @returns {Promise} Return the asset url. */ - uploadAsset({ - appId, - buffer, - filePath - }: UploadAssetFromBufferParams): Promise { + @authenticated + uploadAsset({ appId, buffer, filePath }: UploadAssetFromBufferParams): Promise { const trailingUrl = `asset/${appId}`; - return this.sendFormRequest(HttpMethod.POST, trailingUrl, { - appId, - filePath - }, { - asset: { - filename: filePath, - buffer + return this.sendFormRequest( + HttpMethod.POST, + trailingUrl, + { + appId, + filePath, + }, + { + asset: { + filename: filePath, + buffer, + }, } - }); + ); } /** * Upload the asset file using the data url. - * @param {UploadAssetFromDataUrlParams} UploadAssetFromDataUrlParams + * @param {UploadAssetFromDataUrlParams} UploadAssetFromDataUrlParams * @returns {Promise} Return the asset url. */ + @authenticated uploadAssetWithDataUrl({ appId, dataUrl, - filePath + filePath, }: UploadAssetFromDataUrlParams): Promise { const body = { appId, dataUrl, - filePath + filePath, }; const trailingUrl = `asset/${appId}`; return this.sendRequest(HttpMethod.POST, trailingUrl, body); @@ -275,13 +306,11 @@ export default class Nft extends FactoryBase { /** * Delete the asset you uploaded. - * @param {DeleteAssetParams} DeleteAssetParams + * @param {DeleteAssetParams} DeleteAssetParams */ - deleteAsset({ - appId, - filePath - }: DeleteAssetParams): Promise { - const encodeFilePath = encodeURIComponent(filePath) + @authenticated + deleteAsset({ appId, filePath }: DeleteAssetParams): Promise { + const encodeFilePath = encodeURIComponent(filePath); const trailingUrl = `asset/${appId}/${encodeFilePath}`; return this.sendRequest(HttpMethod.DELETE, trailingUrl); } diff --git a/src/personaModels.ts b/src/personaModels.ts index 5d61e3e2..1c34b63b 100644 --- a/src/personaModels.ts +++ b/src/personaModels.ts @@ -1,5 +1,6 @@ import FactoryBase from './factoryBase'; import { HttpMethod, CreatePersonaModelInfo, ChatResponse, PersonaModelCreditInfo } from './types'; +import { authenticated } from './utils/decorator'; /** * This class supports creating persona models and managing it.\ @@ -14,6 +15,7 @@ export default class PersonaModels extends FactoryBase { * @param coreBeliefs - This is the central content of the persona model. The model reflects this with the highest priority. * @returns Returns the information of the persona model created. */ + @authenticated create( appId: string, userId: string, @@ -39,6 +41,7 @@ export default class PersonaModels extends FactoryBase { * @param messageId (Optional) The ID of message. If you want to manage the message ID separately, set it up. * @returns Returns response of persona model. */ + @authenticated chat( modelId: string, appId: string, @@ -63,6 +66,7 @@ export default class PersonaModels extends FactoryBase { * @param modelId The ID of persona model. * @returns If set, returns credit and burn amount information. */ + @authenticated getCreditInfo( appId: string, modelId: string diff --git a/src/store.ts b/src/store.ts index 018df616..7c0d0d86 100644 --- a/src/store.ts +++ b/src/store.ts @@ -25,6 +25,7 @@ import { itemType, UseItemReturnType, } from './types'; +import { authenticated } from './utils/decorator'; /** * This class supports managing items, store and user items.\ @@ -36,6 +37,7 @@ export default class Store extends FactoryBase { * @param {CreateItemParams} CreateItemParams - The parameters to create a new item. * @returns {Promise} Returns created item information. */ + @authenticated createItem({ appId, type, @@ -66,6 +68,7 @@ export default class Store extends FactoryBase { * Updates an item. * @param {UpdateItemParams} UpdateItemParams - The parameters to update an item. */ + @authenticated updateItem({ appId, itemName, @@ -94,6 +97,7 @@ export default class Store extends FactoryBase { * @param {string} subtype - The subtype of the item. * @param {string} value - The value of the item. */ + @authenticated deregisterItemFromAllStore( appId: string, type: string, @@ -110,6 +114,7 @@ export default class Store extends FactoryBase { * @param {RegisterItemParams} RegisterItemParams - The parameters to register an item. * @returns {Promise} Returns item information registered in the store. */ + @authenticated registerItem({ appId, storeId, @@ -142,6 +147,7 @@ export default class Store extends FactoryBase { * @param {string} storeId - The ID of the store. * @param {string} itemName - The name of the item. */ + @authenticated deregisterItem(appId: string, storeId: string, itemName: string) { const query = { appId }; const trailingUrl = `${storeId}/item/${encodeURIComponent(itemName)}`; @@ -157,6 +163,7 @@ export default class Store extends FactoryBase { * @param {string=} reason The reason for giving an item. ex) Event for NFT holder * @returns {Promise} Returns information of item give history. */ + @authenticated giveItemToUser( appId: string, userId: string, @@ -178,6 +185,7 @@ export default class Store extends FactoryBase { * Updates item in store. * @param {UpdateStoreItemParams} UpdateStoreItemParams - The parameters to update store item. */ + @authenticated updateStoreItem({ appId, storeId, @@ -208,6 +216,7 @@ export default class Store extends FactoryBase { * @param {string} storeId - The ID of the store. * @returns {Promise} List of store items. */ + @authenticated getStoreItemList(appId: string, storeId: string): Promise { const query = { appId }; const trailingUrl = `${storeId}`; @@ -220,6 +229,7 @@ export default class Store extends FactoryBase { * @param {string} userId - The ID of the user. * @returns {Promise} List of user items. */ + @authenticated getUserInventory(appId: string, userId: string): Promise { const query = { appId }; const trailingUrl = `inventory/${userId}`; @@ -233,6 +243,7 @@ export default class Store extends FactoryBase { * @param {string} subtype - The subtype of the items to fetch. * @returns {Promise} List of items. */ + @authenticated getAllItems(appId: string, type?: string, subtype?: string): Promise { const query = { appId, type, subtype }; const trailingUrl = `items`; @@ -246,6 +257,7 @@ export default class Store extends FactoryBase { * @param {string} itemName - The name of the item to fetch. * @returns {Promise} Information about the store item. */ + @authenticated getStoreItemInfo( appId: string, storeId: string, @@ -264,6 +276,7 @@ export default class Store extends FactoryBase { * @param {string} itemName - The name of the item to fetch. * @returns {Promise} Information about the user item. */ + @authenticated getUserItemInfo( appId: string, userId: string, @@ -280,6 +293,7 @@ export default class Store extends FactoryBase { * @param {StorePurchaseParams} StorePurchaseParams - The purchase parameters. * @returns {Promise} Returns purchase history. */ + @authenticated purchaseStoreItem({ appId, storeId, @@ -302,6 +316,7 @@ export default class Store extends FactoryBase { * @param {GetPurchaseHistoryParams} GetPurchaseHistoryParams - The parameters for the request. * @returns {Promise>} - A Promise that resolves to an object containing the purchase history. */ + @authenticated getPurchaseHistory({ appId, year, @@ -321,6 +336,7 @@ export default class Store extends FactoryBase { * @param {GetItemPurchaseHistoryParams} GetItemPurchaseHistoryParams - The parameters for the request. * @returns {Promise>} A Promise that resolves to an object containing the purchase history of the item. */ + @authenticated getItemPurchaseHistory({ appId, itemName, @@ -342,6 +358,7 @@ export default class Store extends FactoryBase { * @param {GetUserPurchaseHistoryParams} GetUserPurchaseHistoryParams - The parameters for the request. * @returns {Promise>} - A Promise that resolves to an object containing the purchase history of the user. */ + @authenticated getUserPurchaseHistory({ appId, userId, @@ -362,6 +379,7 @@ export default class Store extends FactoryBase { * @param {GetItemHistoryParams} GetItemHistoryParams - The parameters for the request. * @returns {Promise>} - A Promise that resolves to an object containing the usage history. */ + @authenticated getItemHistory({ appId, year, @@ -381,6 +399,7 @@ export default class Store extends FactoryBase { * @param {GetSingleItemHistoryParams} GetSingleItemHistoryParams - The parameters for the request. * @returns {Promise>} - A Promise that resolves to an object containing the usage history of the item. */ + @authenticated getSingleItemHistory({ appId, itemName, @@ -402,6 +421,7 @@ export default class Store extends FactoryBase { * @param {GetUserItemHistoryParams} GetUserItemHistoryParams - The parameters for the request. * @returns {Promise>} - A Promise that resolves to an object containing the item usage history of the user. */ + @authenticated getUserItemHistory({ appId, userId, @@ -422,6 +442,7 @@ export default class Store extends FactoryBase { * @param {ItemTryOnParams} ItemTryOnParams - The parameters for trying on an item. * @returns {Promise<{ image: string; isOccupied: boolean }>} - A promise that returns an object containing the image and whether the item is occupied or not. */ + @authenticated tryOnItem({ appId, userId, @@ -451,6 +472,7 @@ export default class Store extends FactoryBase { * @param {ItemUseParams} ItemUseParams - The parameters for using an item. * @returns {UseItemReturnType[T]} - A Promise representing the retrieved item. The return type is determined based on the itemType. */ + @authenticated useItem({ appId, userId, @@ -479,6 +501,7 @@ export default class Store extends FactoryBase { * @param {string} tokenId - The ID of NFT to unequip item. * @returns {Promise} - A promise that returns the metadata of the NFT after unequip nft trait item. */ + @authenticated unequipNftTraitItem( appId: string, userId: string, @@ -510,6 +533,7 @@ export default class Store extends FactoryBase { * @param {string} tokenId - The ID of NFT to unequip item. * @returns {Promise} - A promise that returns the metadata of the NFT after unequip nft trait item. */ + @authenticated resetNftTraitItem( appId: string, userId: string, diff --git a/src/textToArt.ts b/src/textToArt.ts index be8a3c14..4fbd229e 100644 --- a/src/textToArt.ts +++ b/src/textToArt.ts @@ -7,6 +7,7 @@ import { TextToArtTxHash, DiscordMessageInfo, } from './types'; +import { authenticated } from './utils/decorator'; /** * This class supports using text-to-art ai.\ @@ -19,6 +20,7 @@ export default class TextToArt extends FactoryBase { * @param {string} taskId The ID of the text-to-art task. * @returns {Promise} - A promise that resolves with the text-to-art results or null. */ + @authenticated getTextToArtResults(appId: string, taskId: string): Promise { const query = { appId, @@ -33,6 +35,7 @@ export default class TextToArt extends FactoryBase { * @param {string} taskId The ID of the text-to-art task. * @returns {Promise} - A promise that resolves with the request parameters used for the text-to-art task or null. */ + @authenticated getTextToArtParams(appId: string, taskId: string): Promise { const query = { appId, @@ -47,6 +50,7 @@ export default class TextToArt extends FactoryBase { * @param {string} taskId The ID of the text-to-art task. * @returns {Promise} - A promise that resolves with the transaction hash for the task or null. */ + @authenticated getTextToArtTxHash(appId: string, taskId: string): Promise { const query = { appId, @@ -62,6 +66,7 @@ export default class TextToArt extends FactoryBase { * @param {TextToArtParams} params The request parameters used for the text-to-art task. * @returns {Promise} A promise that resolves with the task id for the task or null. */ + @authenticated generateImage(appId: string, discord: DiscordMessageInfo, params: TextToArtParams) { const body = { appId, diff --git a/src/types.ts b/src/types.ts index 36e278c6..07a12f36 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,30 @@ +import { ConnectionCallback, DisconnectionCallback } from '@ainblockchain/ain-js/lib/types'; + +export type ErrorCode = + | 'invalid-argument' + | 'unauthenticated' + | 'permission-denied' + | 'not-found' + | 'already-exists' + | 'precondition-failed' + | 'bad-request' // 400 + | 'forbidden' // 403 + | 'payload-too-large' // 413 + | 'internal' // 500 + | 'unavailable' // 503 + | 'gateway-timeout' // 504 + | 'not-implemented' + | 'unknown'; + +export interface ConnectParams { + /** The connection callback function. */ + connectionCb?: ConnectionCallback; + /** The disconnection callback function. */ + disconnectionCb?: DisconnectionCallback; + /** The custom client ID to set. */ + customClientId?: string; +} + export interface SerializedMessage { code: number; message: string | undefined; @@ -849,6 +876,7 @@ export type NftToken = { tokenURI: string, metadata: NftMetadata, isBurnt: boolean, + [key: string]: any; }; export type NftTokens = { @@ -1019,7 +1047,9 @@ export interface MintNftParams extends Omit { export interface SearchOption { limit?: number, cursor?: string, + order?: 'asc' | 'desc', } + export interface NftSearchParams extends SearchOption { /** The address of the user who owns the AINFT. */ userAddress?: string; @@ -1031,6 +1061,8 @@ export interface NftSearchParams extends SearchOption { name?: string; /** The symbol of AINFT object. */ symbol?: string; + /** The URL slug of AINFT object. (e.g. "My Object" -> "my-object") */ + slug?: string; } export interface getTxBodyTransferNftParams { @@ -1088,9 +1120,9 @@ export interface getTxbodyAddAiHistoryParams { userAddress: string; } -export interface AddAiHistoryParams extends Omit {}; +export interface AddAiHistoryParams extends Omit {} -export interface AinftObjectSearchResponse extends SearchReponse { +export interface AinftObjectSearchResponse extends SearchResponse { ainftObjects: { id: string; name: string; @@ -1099,7 +1131,7 @@ export interface AinftObjectSearchResponse extends SearchReponse { }[]; } -export interface AinftTokenSearchResponse extends SearchReponse { +export interface AinftTokenSearchResponse extends SearchResponse { nfts: { tokenId: string; owner: string; @@ -1109,43 +1141,30 @@ export interface AinftTokenSearchResponse extends SearchReponse { }[]; } -export interface SearchReponse { +export interface SearchResponse { isFinal: boolean; cursor?: string; } -export enum ServiceType { - CHAT = 'chat', +export interface Metadata { + [key: string]: any; } -export enum JobType { - CREATE_ASSISTANT = 'create_assistant', - LIST_ASSISTANTS = 'list_assistants', - RETRIEVE_ASSISTANT = 'retrieve_assistant', - MODIFY_ASSISTANT = 'modify_assistant', - DELETE_ASSISTANT = 'delete_assistant', - - CREATE_THREAD = 'create_thread', - RETRIEVE_THREAD = 'retrieve_thread', - MODIFY_THREAD = 'modify_thread', - DELETE_THREAD = 'delete_thread', - - CREATE_MESSAGE = 'create_message', - LIST_MESSAGES = 'list_messages', - RETRIEVE_MESSAGE = 'retrieve_message', - MODIFY_MESSAGE = 'modify_message', +export interface AinftObjectCreateParams { + /** The name of the AINFT object. */ + name: string; + /** The symbol of the AINFT object. */ + symbol: string; + /** The metadata of the AINFT object. */ + metadata?: Metadata; +} - CREATE_RUN = 'create_run', - LIST_RUNS = 'list_runs', - LIST_RUN_STEPS = 'list_run_steps', - RETRIEVE_RUN = 'retrieve_run', - RETRIEVE_RUN_STEP = 'retrieve_run_step', - MODIFY_RUN = 'modify_run', - CANCEL_RUN = 'cancel_run', +export enum ServiceType { + CHAT = 'chat', } /** - * Nickname of the service. + * @deprecated Nickname of the service. */ export type ServiceNickname = string | 'openai'; @@ -1155,28 +1174,25 @@ export type ServiceNickname = string | 'openai'; * for description of them. * Please note that image-related models are currently not supported. */ -export type OpenAIModel = - | 'gpt-4-1106-preview' - | 'gpt-4' - | 'gpt-4-32k' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-instruct'; +export type Model = + | 'gpt-4o-mini' + | 'gpt-4o' + | 'gpt-4-turbo' + | 'gpt-4'; /** * Represents a transaction result. */ export interface TransactionResult { - tx_hash: string; - result: Record; + tx_hash?: string | null; + result?: Record | null; } /** - * Represents a chat configuration transaction result. + * Represents a ai configuration transaction result. */ -export interface ChatConfigurationTransactionResult extends TransactionResult { - config: ChatConfiguration; +export interface AiConfigurationTransactionResult extends TransactionResult { + config: AiConfiguration; } /** @@ -1229,9 +1245,7 @@ export interface MessagesTransactionResult extends TransactionResult { messages: MessageMap; } -export interface ChatConfiguration { - /** The type of the service. */ - type: ServiceType; +export interface AiConfiguration { /** The name of the service. */ name: string; } @@ -1239,6 +1253,12 @@ export interface ChatConfiguration { export interface Assistant { /** The identifier. */ id: string; + /** The ID of AINFT object. */ + objectId: string | null; + /** The ID of AINFT token. */ + tokenId: string | null; + /** The owner address of AINFT token. */ + owner: string | null; /** The name of the model to use. */ model: string; /** The name of the assistant. */ @@ -1252,6 +1272,8 @@ export interface Assistant { * with keys limited to 64 characters and values to 512 characters. */ metadata: object | null; + /** The metric of the assistant. */ + metric?: { [key: string]: number } | null; /** The UNIX timestamp in seconds. */ created_at: number; } @@ -1265,7 +1287,7 @@ export interface AssistantDeleted { export interface AssistantCreateParams { /** The name of the model to use. */ - model: OpenAIModel; + model: Model; /** The name of the assistant. The maximum length is 256 characters. */ name: string; /** The system instructions that the assistant uses. The maximum length is 32768 characters. */ @@ -1279,9 +1301,14 @@ export interface AssistantCreateParams { metadata?: object | null; } +export interface AssistantCreateOptions { + /** If true, automatically set the profile image for the assistant. */ + image?: boolean; +} + export interface AssistantUpdateParams { /** The name of the model to use. */ - model?: OpenAIModel; + model?: Model; /** The name of the assistant. The maximum length is 256 characters. */ name?: string | null; /** The system instructions that the assistant uses. The maximum length is 32768 characters. */ @@ -1302,7 +1329,7 @@ export interface Thread { * The metadata can contain up to 16 pairs, * with keys limited to 64 characters and values to 512 characters. */ - metadata: object | null; + metadata: object | {}; /** The UNIX timestamp in seconds. */ created_at: number; } @@ -1314,6 +1341,11 @@ export interface ThreadDeleted { deleted: boolean; } +export interface ThreadWithMessages { + thread: Thread; + messages: MessageMap; +} + export interface ThreadCreateParams { /** * The metadata can contain up to 16 pairs, @@ -1365,6 +1397,19 @@ export interface MessageMap { [key: string]: Message; } +export interface QueryParams { + /** The maximum number of items to return. */ + limit?: number; + /** The number of items to skip. */ + offset?: number; + /** The field by which to sort the results. */ + sort?: 'created' | 'updated'; + /** The order of the result set. */ + order?: 'asc' | 'desc'; +} + +export type QueryParamsWithoutSort = Omit; + export interface Page { data: T; first_id: string; @@ -1391,3 +1436,12 @@ export interface MessageUpdateParams { */ metadata?: object | null; } + +export type EnvType = 'dev' | 'prod'; + +export enum TokenStatus { + MINTED = 'minted', + ASSISTANT_CREATED = 'assistant_created', + THREAD_CREATED = 'thread_created', + MESSAGE_CREATED = 'message_created', +} diff --git a/src/utils/ainize.ts b/src/utils/ainize.ts new file mode 100644 index 00000000..bf3ef5cc --- /dev/null +++ b/src/utils/ainize.ts @@ -0,0 +1,102 @@ +import Ain from '@ainblockchain/ain-js'; +import Ainize from '@ainize-team/ainize-js'; +import Service from '@ainize-team/ainize-js/dist/service'; +import Handler from '@ainize-team/ainize-js/dist/handlers/handler'; +import { DEFAULT_AINIZE_SERVICE_NAME } from '../constants'; +import { AinftError } from '../error'; + +const DEFAULT_TIMEOUT_MS = 60 * 1000; // 1min + +export const getService = async (ainize: Ainize, name: string): Promise => { + const server = await ainize.getService(name); + const isRunning = await server.isRunning(); + if (!isRunning) { + throw new AinftError('unavailable', `service ${name} is not running.`); + } + return server; +}; + +export const getServiceName = () => { + return DEFAULT_AINIZE_SERVICE_NAME; +}; + +export const request = async ( + ainize: Ainize, + { serviceName, opType, data, timeout = DEFAULT_TIMEOUT_MS }: AinizeRequest +): Promise> => { + if (!Handler.getInstance().isConnected()) { + // NOTE(jiyoung): client error handling method need to be updated. + throw new AinftError('unavailable', 'connection to the blockchain network could not be established.'); + } + + let timer; + const startTimer = new Promise( + (reject) => (timer = setTimeout(() => reject(`timeout of ${timeout}ms exceeded`), timeout)) + ); + + const server = await getService(ainize, serviceName); + try { + const response = await Promise.race([server.request({ ...data, jobType: opType }), startTimer]); + if (response.status === AinizeStatus.FAIL) { + throw new AinftError('internal', JSON.stringify(response.data)); + } + return response as AinizeResponse; + } catch (error: any) { + throw new AinftError('internal', `failed to ${opType}: ${error.message}`); + } finally { + if (timer) { + clearTimeout(timer); + } + } +}; + +export interface AinizeRequest { + serviceName: string; + opType: OperationType; + data?: any; + timeout?: number; +} + +export interface AinizeResponse { + data: T; + status: AinizeStatus; +} + +export enum AinizeStatus { + SUCCESS = 'SUCCESS', + FAIL = 'FAIL', +} + +export enum OperationType { + CREATE_ASSISTANT = 'create_assistant', + LIST_ASSISTANTS = 'list_assistants', + RETRIEVE_ASSISTANT = 'retrieve_assistant', + MODIFY_ASSISTANT = 'modify_assistant', + DELETE_ASSISTANT = 'delete_assistant', + MINT_CREATE_ASSISTANT = 'mint_create_assistant', + + CREATE_THREAD = 'create_thread', + LIST_THREADS = 'list_threads', + RETRIEVE_THREAD = 'retrieve_thread', + MODIFY_THREAD = 'modify_thread', + DELETE_THREAD = 'delete_thread', + CREATE_RUN_THREAD = 'create_run_thread', + + CREATE_MESSAGE = 'create_message', + LIST_MESSAGES = 'list_messages', + RETRIEVE_MESSAGE = 'retrieve_message', + MODIFY_MESSAGE = 'modify_message', + SEND_MESSAGE = 'send_message', + + CREATE_RUN = 'create_run', + LIST_RUNS = 'list_runs', + LIST_RUN_STEPS = 'list_run_steps', + RETRIEVE_RUN = 'retrieve_run', + RETRIEVE_RUN_STEP = 'retrieve_run_step', + MODIFY_RUN = 'modify_run', + CANCEL_RUN = 'cancel_run', + + CREATE_USER = 'create_user', + GET_CREDIT = 'get_credit', + MINT_TOKEN = 'mint_token', +} diff --git a/src/utils/decorator.ts b/src/utils/decorator.ts new file mode 100644 index 00000000..a5cb24ab --- /dev/null +++ b/src/utils/decorator.ts @@ -0,0 +1,18 @@ +import Ain from '@ainblockchain/ain-js'; +import { AinftError } from '../error'; + +export function authenticated(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const method = descriptor.value; + descriptor.value = function (...args: any[]) { + try { + const ain = (this as any).ain as Ain; + ain.signer.getAddress(); + } catch (error) { + throw new AinftError( + 'unauthenticated', + `method '${propertyKey}' must be set a private key or signer.` + ); + } + return method.apply(this, args); + }; +} diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 00000000..a4b6ce9d --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,15 @@ +import { AinftError } from '../error'; +import { EnvType } from '../types'; + +let env: EnvType | null; + +export const getEnv = () => { + if (!env) { + throw new AinftError('bad-request', 'env is not defined. please call setEnv() first.'); + } + return env; +}; + +export const setEnv = (value: EnvType) => { + env = value; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..c2de4ba0 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './env'; +export * from './path'; +export * from './transaction'; +export * from './util'; +export * from './validator'; diff --git a/src/utils/path.ts b/src/utils/path.ts new file mode 100644 index 00000000..ce62d77c --- /dev/null +++ b/src/utils/path.ts @@ -0,0 +1,82 @@ +export const Path = { + apps: () => { + return { + value: () => '/apps', + }; + }, + app: (appId: string) => { + return { + value: () => `${Path.apps().value()}/${appId}`, + ai: (serviceName: string) => { + return { + value: () => `${Path.app(appId).value()}/ai/${serviceName}`, + }; + }, + tokens: () => { + return { + value: () => `${Path.app(appId).value()}/tokens`, + }; + }, + token: (tokenId: string) => { + return { + value: () => `${Path.app(appId).tokens().value()}/${tokenId}`, + ai: () => { + return { + value: () => `${Path.app(appId).token(tokenId).value()}/ai`, + config: () => { + return { + value: () => `${Path.app(appId).token(tokenId).ai().value()}/config`, + }; + }, + history: (address: string) => { + return { + value: () => `${Path.app(appId).token(tokenId).ai().value()}/history/${address}`, + threads: () => { + return { + value: () => + `${Path.app(appId).token(tokenId).ai().history(address).value()}/threads`, + }; + }, + thread: (threadId: string) => { + return { + value: () => + `${Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .threads() + .value()}/${threadId}`, + messages: () => { + return { + value: () => + `${Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .value()}/messages`, + }; + }, + message: (messageId: string) => { + return { + value: () => + `${Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .messages() + .value()}/${messageId}`, + }; + }, + }; + }, + }; + }, + }; + }, + }; + }, + }; + }, +}; diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts new file mode 100644 index 00000000..217041bc --- /dev/null +++ b/src/utils/transaction.ts @@ -0,0 +1,73 @@ +import Ain from '@ainblockchain/ain-js'; +import { TransactionInput, SetOperation, SetMultiOperation } from '@ainblockchain/ain-js/lib/types'; +import { MIN_GAS_PRICE, TX_BYTES_LIMIT } from '../constants'; +import { AinftError } from '../error'; + +export const buildSetValueOp = (ref: string, value: any): SetOperation => ({ + type: 'SET_VALUE', + ref, + value, +}); + +export const buildSetRuleOp = (ref: string, rule: { write?: any; state?: any }): SetOperation => ({ + type: 'SET_RULE', + ref, + value: { + '.rule': { + write: rule.write, + state: rule.state, + }, + }, +}); + +export const buildSetWriteRuleOp = (ref: string, rule: any) => buildSetRuleOp(ref, { write: rule }); + +export const buildSetStateRuleOp = (ref: string, rule: any) => buildSetRuleOp(ref, { state: rule }); + +export const buildSetOp = (opList: SetOperation[]): SetMultiOperation => ({ + type: 'SET', + op_list: opList, +}); + +export const buildSetTxBody = ( + operation: SetOperation | SetMultiOperation, + address: string +): TransactionInput => ({ + operation, + address, + gas_price: MIN_GAS_PRICE, + nonce: -1, +}); + +export const isTxSizeValid = (txBody: TransactionInput) => { + const text = JSON.stringify(txBody); + const size = new TextEncoder().encode(text).length; + return size <= TX_BYTES_LIMIT; +}; + +export const isTxSuccess = (txResult: any) => { + const { result } = txResult; + if (result.code && result.code !== 0) { + return false; + } + if (result.result_list) { + const results = Object.values(result.result_list); + return results.every((_result: any) => _result.code === 0); + } + return true; +}; + +export const sendTx = async (txBody: TransactionInput, ain: Ain) => { + if (!isTxSizeValid(txBody)) { + throw new AinftError( + 'payload-too-large', + `transaction exceeds size limit: ${TX_BYTES_LIMIT} bytes` + ); + } + const result = await ain.sendTransaction(txBody); + if (!isTxSuccess(result)) { + console.error(JSON.stringify(result, null, 2)); + throw new AinftError('internal', `failed to send transaction: ${result.tx_hash}`); + } + return result; +}; diff --git a/src/utils/util.ts b/src/utils/util.ts new file mode 100644 index 00000000..15055e8a --- /dev/null +++ b/src/utils/util.ts @@ -0,0 +1,85 @@ +import stringify = require('fast-json-stable-stringify'); +import Ain from '@ainblockchain/ain-js'; +import { + SetOperation, + SetMultiOperation, + TransactionInput, + GetOptions, +} from '@ainblockchain/ain-js/lib/types'; +import * as ainUtil from '@ainblockchain/ain-util'; + +import { MIN_GAS_PRICE } from '../constants'; +import { HttpMethod } from '../types'; +import { Path } from './path'; +import { AinftError } from '../error'; + +export const buildData = ( + method: HttpMethod, + path: string, + timestamp: number, + data?: Record +) => { + const _data: any = { + method, + path, + timestamp, + }; + + if (!data || Object.keys(data).length === 0) { + return _data; + } + + if (method === HttpMethod.POST || method === HttpMethod.PUT) { + _data['body'] = stringify(data); + } else { + _data['querystring'] = stringify(data); + } + + return _data; +}; + +export function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +export function serializeEndpoint(endpoint: string) { + return endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint; +} + +export const valueExists = async (ain: Ain, path: string): Promise => { + return !!(await ain.db.ref(path).getValue()); +}; + +export const getAssistant = async (ain: Ain, appId: string, tokenId: string) => { + // TODO(jiyoung): fix circular reference with Ainft721Object.getAppId. + // const appId = AinftObject.getAppId(objectId); + const assistantPath = Path.app(appId).token(tokenId).ai().value(); + const assistant = await getValue(ain, assistantPath); + if (!assistant) { + throw new AinftError('not-found', `assistant not found: ${appId}(${tokenId})`); + } + return assistant; +}; + +export const getToken = async (ain: Ain, appId: string, tokenId: string) => { + const tokenPath = Path.app(appId).token(tokenId).value(); + const token = await getValue(ain, tokenPath); + if (!token) { + throw new AinftError('not-found', `token not found: ${appId}(${tokenId})`); + } + return token; +}; + +export const getValue = async (ain: Ain, path: string, options?: GetOptions): Promise => { + return ain.db.ref().getValue(path, options); +}; + +export const getChecksumAddress = (address: string): string => { + return ainUtil.toChecksumAddress(address); +}; + +export const isJoiError = (error: any) => { + return error.response?.data?.isJoiError === true; +}; diff --git a/src/utils/validator.ts b/src/utils/validator.ts new file mode 100644 index 00000000..79519ffc --- /dev/null +++ b/src/utils/validator.ts @@ -0,0 +1,116 @@ +import Ain from '@ainblockchain/ain-js'; +import AinftObject from '../ainft721Object'; +import { MessageMap } from '../types'; +import { Path } from './path'; +import { valueExists, getValue } from './util'; +import { AinftError } from '../error'; + +export const isObjectOwner = async (ain: Ain, objectId: string, address: string) => { + const appId = AinftObject.getAppId(objectId); + const objectOwnerPath = `apps/${appId}/owner`; + const objectOwner = await getValue(ain, objectOwnerPath); + return address === objectOwner; +}; + +export const validateObject = async (ain: Ain, objectId: string) => { + const appId = AinftObject.getAppId(objectId); + const objectPath = Path.app(appId).value(); + const object = await getValue(ain, objectPath, { is_shallow: true }); + if (!object) { + throw new AinftError('not-found', `object not found: ${objectId}`); + } +}; + +export const validateServerConfigurationForObject = async ( + ain: Ain, + objectId: string, + serviceName: string +) => { + const appId = AinftObject.getAppId(objectId); + const configPath = Path.app(appId).ai(serviceName).value(); + if (!(await valueExists(ain, configPath))) { + throw new AinftError( + 'precondition-failed', + `service configuration is missing for ${objectId}.` + ); + } +}; + +export const validateObjectOwner = async (ain: Ain, objectId: string, address: string) => { + if (!isObjectOwner(ain, objectId, address)) { + throw new AinftError('permission-denied', `${address} do not have owner permission.`); + } +}; + +export const validateToken = async (ain: Ain, objectId: string, tokenId: string) => { + const appId = AinftObject.getAppId(objectId); + const tokenPath = Path.app(appId).token(tokenId).value(); + if (!(await valueExists(ain, tokenPath))) { + throw new AinftError('not-found', `token not found: ${objectId}(${tokenId})`); + } +}; + +export const validateDuplicateAssistant = async (ain: Ain, objectId: string, tokenId: string) => { + const appId = AinftObject.getAppId(objectId); + const assistantPath = Path.app(appId).token(tokenId).ai().value(); + if (await valueExists(ain, assistantPath)) { + throw new AinftError('already-exists', 'assistant already exists.'); + } +}; + +export const validateAssistant = async ( + ain: Ain, + objectId: string, + tokenId: string, + assistantId?: string +) => { + const appId = AinftObject.getAppId(objectId); + const assistantPath = Path.app(appId).token(tokenId).ai().value(); + const assistant = await getValue(ain, assistantPath); + if (!assistant) { + throw new AinftError('not-found', `assistant not found: ${assistantId}`); + } + if (assistantId && assistantId !== assistant.id) { + throw new AinftError('bad-request', `invalid assistant id: ${assistantId} != ${assistant.id}`); + } +}; + +export const validateThread = async ( + ain: Ain, + objectId: string, + tokenId: string, + address: string, + threadId: string +) => { + const appId = AinftObject.getAppId(objectId); + const threadPath = Path.app(appId).token(tokenId).ai().history(address).thread(threadId).value(); + if (!(await valueExists(ain, threadPath))) { + throw new AinftError('not-found', `thread not found: ${threadId}`); + } +}; + +export const validateMessage = async ( + ain: Ain, + objectId: string, + tokenId: string, + address: string, + threadId: string, + messageId: string +) => { + const appId = AinftObject.getAppId(objectId); + const messagesPath = Path.app(appId) + .token(tokenId) + .ai() + .history(address) + .thread(threadId) + .messages() + .value(); + const messages: MessageMap = await getValue(ain, messagesPath); + // TODO(jiyoung): optimize inefficient loop. + for (const key in messages) { + if (messages[key].id === messageId) { + return; + } + } + throw new AinftError('not-found', `message not found: ${threadId}(${messageId})`); +}; diff --git a/test/ai/assistant.test.ts b/test/ai/assistant.test.ts new file mode 100644 index 00000000..88c5806f --- /dev/null +++ b/test/ai/assistant.test.ts @@ -0,0 +1,90 @@ +import AinftJs from '../../src/ainft'; +import { address, assistantId, objectId, privateKey, tokenId } from '../test_data'; +import { ASSISTANT_REGEX, TX_HASH_REGEX } from '../constants'; + +describe.skip('assistant', () => { + let ainft: AinftJs; + + beforeAll(async () => { + ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, + }); + await ainft.connect(); + }); + + afterAll(async () => { + await ainft.disconnect(); + }); + + it('should create assistant', async () => { + const result = await ainft.assistant.create(objectId, tokenId, { + model: 'gpt-4o-mini', + name: 'name', + instructions: 'instructions', + description: 'description', + metadata: { key1: 'value1' }, + }); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.assistant.id).toMatch(ASSISTANT_REGEX); + expect(result.assistant.model).toBe('gpt-4o-mini'); + expect(result.assistant.name).toBe('name'); + expect(result.assistant.instructions).toBe('instructions'); + expect(result.assistant.description).toBe('description'); + expect(result.assistant.metadata).toEqual({ key1: 'value1' }); + }); + + it('should get assistant', async () => { + const assistant = await ainft.assistant.get(objectId, tokenId, assistantId); + + expect(assistant.id).toBe(assistantId); + expect(assistant.model).toBe('gpt-4o-mini'); + expect(assistant.name).toBe('name'); + expect(assistant.instructions).toBe('instructions'); + expect(assistant.description).toBe('description'); + expect(assistant.metadata).toEqual({ key1: 'value1' }); + }); + + it('should list assistants', async () => { + const result = await ainft.assistant.list([objectId], null); + + expect(result.total).toBeDefined(); + expect(result.items).toBeDefined(); + }); + + it('should update assistant', async () => { + const result = await ainft.assistant.update(objectId, tokenId, assistantId, { + model: 'gpt-4', + name: 'new_name', + instructions: 'new_instructions', + description: 'new_description', + metadata: { key1: 'value1', key2: 'value2' }, + }); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.assistant.id).toBe(assistantId); + expect(result.assistant.model).toBe('gpt-4'); + expect(result.assistant.name).toBe('new_name'); + expect(result.assistant.instructions).toBe('new_instructions'); + expect(result.assistant.description).toBe('new_description'); + expect(result.assistant.metadata).toEqual({ key1: 'value1', key2: 'value2' }); + }); + + it('should delete assistant', async () => { + const result = await ainft.assistant.delete(objectId, tokenId, assistantId); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.delAssistant.id).toBe(assistantId); + expect(result.delAssistant.deleted).toBe(true); + }); + + it('should mint assistant', async () => { + const result = await ainft.assistant.mint(objectId, address); + }); +}); diff --git a/test/ai/chat.test.ts b/test/ai/chat.test.ts new file mode 100644 index 00000000..6af6f68e --- /dev/null +++ b/test/ai/chat.test.ts @@ -0,0 +1,51 @@ +import AinftJs from '../../src/ainft'; +import { privateKey, address, objectId, serviceName } from '../test_data'; +import { TX_HASH_REGEX } from '../constants'; + +describe.skip('chat', () => { + let ainft: AinftJs; + + beforeAll(async () => { + ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, + }); + await ainft.connect(); + }); + + afterAll(async () => { + await ainft.disconnect(); + }); + + it('should configure ai', async () => { + const result = await ainft.ai.configure(objectId, serviceName); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.config).toEqual({ name: serviceName }); + }); + + it('should get credit', async () => { + const credit = await ainft.ai.getCredit(serviceName); + + expect(credit).toBe(null); + }); + + // NOTE(jiyoung): deposit is disabled until withdrawal is implemented. + // it('should deposit credit', async () => { + // const result = await ainft.ai.depositCredit(serviceName, 10); + + // expect(result.tx_hash).toMatch(TX_HASH_REGEX); + // expect(result.address).toBe(address); + // expect(result.balance).toBe(10); + // }); + + it('should all tokens with status', async () => { + const result = await ainft.ai.getUserTokensByStatus(objectId, address); + + expect(result.total).toBeDefined(); + expect(result.items).toBeDefined(); + }); +}); diff --git a/test/ai/message.test.ts b/test/ai/message.test.ts new file mode 100644 index 00000000..f1f3d20c --- /dev/null +++ b/test/ai/message.test.ts @@ -0,0 +1,71 @@ +import AinftJs from '../../src/ainft'; +import { messageId, objectId, privateKey, threadId, tokenId, address } from '../test_data'; +import { MESSAGE_REGEX, TX_HASH_REGEX } from '../constants'; + +jest.setTimeout(60 * 1000); // 1min + +describe.skip('message', () => { + let ainft: AinftJs; + + beforeAll(async () => { + ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, + }); + await ainft.connect(); + }); + + afterAll(async () => { + await ainft.disconnect(); + }); + + it('should create message', async () => { + const result = await ainft.message.create(objectId, tokenId, threadId, { + role: 'user', + content: 'Hello world!', + metadata: { key1: 'value1' }, + }); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(Object.keys(result.messages).length).toBe(2); + expect(result.messages[1].id).toMatch(MESSAGE_REGEX); + expect(result.messages[1].thread_id).toBe(threadId); + expect(result.messages[1].role).toBe('user'); + expect(result.messages[1].content[0].text.value).toBe('Hello world!'); + expect(result.messages[1].metadata).toEqual({ key1: 'value1' }); + }); + + it('should get message', async () => { + const message = await ainft.message.get(objectId, tokenId, threadId, messageId, address); + + expect(message.id).toBe(messageId); + expect(message.thread_id).toBe(threadId); + expect(message.role).toBe('user'); + expect(message.content[0].text.value).toBe('Hello world!'); + expect(message.metadata).toEqual({ key1: 'value1' }); + }); + + it('should list messages', async () => { + const messages = await ainft.message.list(objectId, tokenId, threadId, address); + + expect(Object.keys(messages).length).toBe(2); + }); + + it('should update message', async () => { + const body = { metadata: { key1: 'value1', key2: 'value2' } }; + + const result = await ainft.message.update(objectId, tokenId, threadId, messageId, { + metadata: { key1: 'value1', key2: 'value2' }, + }); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.message.id).toBe(messageId); + expect(result.message.role).toBe('user'); + expect(result.message.content[0].text.value).toEqual('Hello world!'); + expect(result.message.metadata).toEqual({ key1: 'value1', key2: 'value2' }); + }); +}); diff --git a/test/ai/thread.test.ts b/test/ai/thread.test.ts new file mode 100644 index 00000000..8667c1c5 --- /dev/null +++ b/test/ai/thread.test.ts @@ -0,0 +1,65 @@ +import AinftJs from '../../src/ainft'; +import { privateKey, address, objectId, tokenId, threadId } from '../test_data'; +import { TX_HASH_REGEX, THREAD_REGEX } from '../constants'; + +describe.skip('thread', () => { + let ainft: AinftJs; + + beforeAll(async () => { + ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, + }); + await ainft.connect(); + }); + + afterAll(async () => { + await ainft.disconnect(); + }); + + it('should create thread', async () => { + const result = await ainft.thread.create(objectId, tokenId, { + metadata: { key1: 'value1' }, + }); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.thread.id).toMatch(THREAD_REGEX); + expect(result.thread.metadata).toEqual({ key1: 'value1' }); + }); + + it('should get thread', async () => { + const thread = await ainft.thread.get(objectId, tokenId, threadId, address); + + expect(thread.id).toBe(threadId); + expect(thread.metadata).toEqual({ key1: 'value1' }); + }); + + it('should list threads', async () => { + const result = await ainft.thread.list([objectId], null, null, { limit: 20, offset: 0, order: 'desc' }); + + expect(result.items).toBeDefined(); + }); + + it('should update thread', async () => { + const result = await ainft.thread.update(objectId, tokenId, threadId, { + metadata: { key1: 'value1', key2: 'value2' }, + }); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.thread.id).toBe(threadId); + expect(result.thread.metadata).toEqual({ key1: 'value1', key2: 'value2' }); + }); + + it('should delete thread', async () => { + const result = await ainft.thread.delete(objectId, tokenId, threadId); + + expect(result.tx_hash).toMatch(TX_HASH_REGEX); + expect(result.result).toBeDefined(); + expect(result.delThread.id).toBe(threadId); + expect(result.delThread.deleted).toBe(true); + }); +}); diff --git a/test/assistants.test.ts b/test/assistants.test.ts deleted file mode 100644 index 25457abc..00000000 --- a/test/assistants.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import AinftJs from '../src/ainft'; -import { AssistantCreateParams, AssistantUpdateParams } from '../src/types'; -import { test_private_key, test_object_id, test_token_id, test_assistant_id } from './test_data'; - -jest.mock('../src/common/util', () => { - const mockRequest = jest.fn((jobType, body) => { - switch (jobType) { - case 'create_assistant': - case 'modify_assistant': - return { ...body, id: test_assistant_id, created_at: 0 }; - case 'retrieve_assistant': - return { - id: test_assistant_id, - model: 'gpt-3.5-turbo', - name: 'name', - instructions: 'instructions', - description: 'description', - metadata: { key1: 'value1' }, - created_at: 0, - }; - case 'delete_assistant': - return { - id: test_assistant_id, - deleted: true, - }; - default: - return null; - } - }); - const util = jest.requireActual('../src/common/util'); - return { - ...util, - validateAssistant: jest.fn().mockResolvedValue(undefined), - validateAssistantNotExists: jest.fn().mockResolvedValue(undefined), - sendAinizeRequest: mockRequest, - sendTransaction: jest.fn().mockResolvedValue({ - tx_hash: '0x' + 'a'.repeat(64), - result: { code: 0 }, - }), - }; -}); - -const TX_PATTERN = /^0x([A-Fa-f0-9]{64})$/; -const ASST_PATTERN = /^asst_([A-Za-z0-9]{24})$/; - -jest.setTimeout(60000); - -describe.skip('assistant', () => { - let ainft: AinftJs; - - beforeAll(() => { - ainft = new AinftJs(test_private_key, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, - }); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - it('should create assistant', async () => { - const body: AssistantCreateParams = { - model: 'gpt-3.5-turbo', - name: 'name', - instructions: 'instructions', - description: 'description', - metadata: { key1: 'value1' }, - }; - - const res = await ainft.chat.assistant.create(test_object_id, test_token_id, 'openai', body); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.assistant.id).toMatch(ASST_PATTERN); - expect(res.assistant.model).toBe('gpt-3.5-turbo'); - expect(res.assistant.name).toBe('name'); - expect(res.assistant.instructions).toBe('instructions'); - expect(res.assistant.description).toBe('description'); - expect(res.assistant.metadata).toEqual({ key1: 'value1' }); - }); - - it('should get assistant', async () => { - const assistant = await ainft.chat.assistant.get( - test_assistant_id, - test_object_id, - test_token_id, - 'openai' - ); - - expect(assistant.id).toBe(test_assistant_id); - expect(assistant.model).toBe('gpt-3.5-turbo'); - expect(assistant.name).toBe('name'); - expect(assistant.instructions).toBe('instructions'); - expect(assistant.description).toBe('description'); - expect(assistant.metadata).toEqual({ key1: 'value1' }); - }); - - it('should update assistant', async () => { - const body: AssistantUpdateParams = { - model: 'gpt-4', - name: 'new_name', - instructions: 'new_instructions', - description: 'new_description', - metadata: { key1: 'value1', key2: 'value2' }, - }; - - const res = await ainft.chat.assistant.update( - test_assistant_id, - test_object_id, - test_token_id, - 'openai', - body - ); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.assistant.id).toBe(test_assistant_id); - expect(res.assistant.model).toBe('gpt-4'); - expect(res.assistant.name).toBe('new_name'); - expect(res.assistant.instructions).toBe('new_instructions'); - expect(res.assistant.description).toBe('new_description'); - expect(res.assistant.metadata).toEqual({ key1: 'value1', key2: 'value2' }); - }); - - it('should delete assistant', async () => { - const res = await ainft.chat.assistant.delete( - test_assistant_id, - test_object_id, - test_token_id, - 'openai' - ); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.delAssistant.id).toBe(test_assistant_id); - expect(res.delAssistant.deleted).toBe(true); - }); -}); diff --git a/test/chat.test.ts b/test/chat.test.ts deleted file mode 100644 index 49d026db..00000000 --- a/test/chat.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import AinftJs from '../src/ainft'; -import { test_private_key, test_address, test_object_id, test_service_name } from './test_data'; - -jest.mock('../src/common/util', () => { - const util = jest.requireActual('../src/common/util'); - return { - ...util, - ainizeLogin: jest.fn().mockResolvedValue(undefined), - ainizeLogout: jest.fn().mockResolvedValue(undefined), - validateAndGetService: jest.fn().mockResolvedValue({ - getCreditBalance: jest - .fn() - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(null) - .mockResolvedValue(10), - chargeCredit: jest.fn().mockResolvedValue('0x' + 'a'.repeat(64)), - }), - sendTransaction: jest.fn().mockResolvedValue({ - tx_hash: '0x' + 'a'.repeat(64), - result: { code: 0 }, - }), - }; -}); - -const TX_PATTERN = /^0x([A-Fa-f0-9]{64})$/; - -jest.setTimeout(60000); - -describe.skip('chat', () => { - let ainft: AinftJs; - - beforeAll(() => { - ainft = new AinftJs(test_private_key, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, - }); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - it('should configure chat', async () => { - const res = await ainft.chat.configure(test_object_id, 'openai'); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.config.name).toBe(test_service_name); - expect(res.config.type).toBe('chat'); - }); - - it('should get credit', async () => { - const credit = await ainft.chat.getCredit('openai'); - - expect(credit).toBe(null); - }); - - it('should deposit credit', async () => { - const res = await ainft.chat.depositCredit('openai', 10); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.address).toBe(test_address); - expect(res.balance).toBe(10); - }); -}); diff --git a/test/constants.ts b/test/constants.ts new file mode 100644 index 00000000..0be24f6c --- /dev/null +++ b/test/constants.ts @@ -0,0 +1,5 @@ +export const TX_HASH_REGEX = /^0x([A-Fa-f0-9]{64})$/; +export const AINFT_OBJECT_REGEX = /^0x([A-Fa-f0-9]{40})$/; +export const ASSISTANT_REGEX = /^asst_([A-Za-z0-9]{24})$/; +export const THREAD_REGEX = /^thread_([A-Za-z0-9]{24})$/; +export const MESSAGE_REGEX = /^msg_([A-Za-z0-9]{24})$/; diff --git a/test/messages.test.ts b/test/messages.test.ts deleted file mode 100644 index bc33ce82..00000000 --- a/test/messages.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import AinftJs from '../src/ainft'; -import { MessageCreateParams } from '../src/types'; -import { - test_message_id, - test_object_id, - test_private_key, - test_thread_id, - test_token_id, -} from './test_data'; - -jest.mock('../src/common/util', () => { - const mockRequest = jest.fn((jobType) => { - switch (jobType) { - case 'create_message': - return Promise.resolve({ status: 'SUCCESS', data: {} }); - case 'retrieve_message': - return Promise.resolve({ - status: 'SUCCESS', - data: { - id: 'msg_000000000000000000000001', - created_at: 1707360317, - thread_id: 'thread_000000000000000000000001', - role: 'user', - content: { - '0': { - type: 'text', - text: { - value: 'Hello world!', - }, - }, - }, - assistant_id: null, - run_id: null, - metadata: { - key1: 'value1', - }, - }, - }); - case 'modify_message': - return Promise.resolve({ - status: 'SUCCESS', - data: { - id: 'msg_000000000000000000000001', - created_at: 1707360317, - thread_id: 'thread_000000000000000000000001', - role: 'user', - content: { - '0': { - type: 'text', - text: { - value: 'Hello world!', - }, - }, - }, - assistant_id: null, - run_id: null, - metadata: { - key1: 'value1', - key2: 'value2', - }, - }, - }); - case 'list_messages': - return Promise.resolve({ - status: 'SUCCESS', - data: { - data: { - '0': { - id: 'msg_000000000000000000000002', - created_at: 1707360319, - thread_id: 'thread_000000000000000000000001', - role: 'assistant', - content: { - '0': { - type: 'text', - text: { - value: 'Hello! How can I assist you today?', - }, - }, - }, - assistant_id: 'asst_000000000000000000000001', - run_id: 'run_000000000000000000000001', - }, - '1': { - id: 'msg_000000000000000000000001', - created_at: 1707360317, - thread_id: 'thread_000000000000000000000001', - role: 'user', - content: { - '0': { - type: 'text', - text: { - value: 'Hello world!', - }, - }, - }, - assistant_id: null, - run_id: null, - metadata: { - key1: 'value1', - }, - }, - }, - has_more: false, - }, - }); - case 'create_run': - return Promise.resolve({ - status: 'SUCCESS', - data: { - id: 'run_000000000000000000000001', - }, - }); - case 'retrieve_run': - return Promise.resolve({ - status: 'SUCCESS', - data: { - id: 'run_000000000000000000000001', - status: 'completed', - }, - }); - default: - return null; - } - }); - const util = jest.requireActual('../src/common/util'); - return { - ...util, - validateAndGetAssistant: jest.fn().mockResolvedValue({ id: '1' }), - validateThread: jest.fn().mockResolvedValue(undefined), - validateMessage: jest.fn().mockResolvedValue(undefined), - validateAndGetService: jest.fn().mockResolvedValue({ - request: jest.fn(({ jobType }) => { - return mockRequest(jobType); - }), - }), - sendTransaction: jest.fn().mockResolvedValue({ - tx_hash: '0x' + 'a'.repeat(64), - result: { code: 0 }, - }), - }; -}); - -const TX_PATTERN = /^0x([A-Fa-f0-9]{64})$/; -const MSG_PATTERN = /^msg_([A-Za-z0-9]{24})$/; - -jest.setTimeout(60000); - -describe.skip('message', () => { - let ainft: AinftJs; - - beforeAll(async () => { - ainft = new AinftJs(test_private_key, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, - }); - }); - - afterAll(async () => { - jest.restoreAllMocks(); - }); - - it('should create message', async () => { - const body: MessageCreateParams = { - role: 'user', - content: 'Hello world!', - metadata: { key1: 'value1' }, - }; - - const res = await ainft.chat.message.create( - test_thread_id, - test_object_id, - test_token_id, - 'openai', - body - ); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(Object.keys(res.messages).length).toBe(2); - expect(res.messages[1].id).toMatch(MSG_PATTERN); - expect(res.messages[1].thread_id).toBe(test_thread_id); - expect(res.messages[1].role).toBe('user'); - expect(res.messages[1].content[0].text.value).toBe('Hello world!'); - expect(res.messages[1].metadata).toEqual({ key1: 'value1' }); - }); - - it('should get message', async () => { - const message = await ainft.chat.message.get( - test_message_id, - test_thread_id, - test_object_id, - test_token_id, - 'openai' - ); - - expect(message.id).toBe(test_message_id); - expect(message.thread_id).toBe(test_thread_id); - expect(message.role).toBe('user'); - expect(message.content[0].text.value).toBe('Hello world!'); - expect(message.metadata).toEqual({ key1: 'value1' }); - }); - - it('should get message list', async () => { - const messages = await ainft.chat.message.list( - test_thread_id, - test_object_id, - test_token_id, - 'openai' - ); - - expect(Object.keys(messages).length).toBe(2); - }); - - it('should update message', async () => { - const body = { metadata: { key1: 'value1', key2: 'value2' } }; - - const res = await ainft.chat.message.update( - test_message_id, - test_thread_id, - test_object_id, - test_token_id, - 'openai', - body - ); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.message.id).toBe(test_message_id); - expect(res.message.role).toBe('user'); - expect(res.message.content[0].text.value).toEqual('Hello world!'); - expect(res.message.metadata).toEqual({ key1: 'value1', key2: 'value2' }); - }); -}); diff --git a/test/nft.test.ts b/test/nft.test.ts new file mode 100644 index 00000000..633b8603 --- /dev/null +++ b/test/nft.test.ts @@ -0,0 +1,45 @@ +import AinftJs from '../src/ainft'; +import { AINFT_OBJECT_REGEX, TX_HASH_REGEX } from './constants'; +import { address, privateKey } from './test_data'; + +describe.skip('nft', () => { + const ainft = new AinftJs({ + privateKey, + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, + }); + + it('should create ainft object', async () => { + const result = await ainft.nft.create({ + name: 'name', + symbol: 'symbol', + metadata: { + author: { + address: address, + username: 'username', + }, + description: 'description', + logoImage: 'https://picsum.photos/200/200', + bannerImage: 'https://picsum.photos/1400/264', + externalLink: 'https://example.com', + }, + }); + + expect(result.txHash).toMatch(TX_HASH_REGEX); + expect(result.ainftObject.id).toMatch(AINFT_OBJECT_REGEX); + expect(result.ainftObject.name).toBe('name'); + expect(result.ainftObject.symbol).toBe('symbol'); + expect(result.ainftObject.owner).toBe(address); + expect(result.ainftObject.metadata).toEqual({ + author: { + address: address, + username: 'username', + }, + description: 'description', + logoImage: 'https://picsum.photos/200/200', + bannerImage: 'https://picsum.photos/1400/264', + externalLink: 'https://example.com', + }); + }); +}); diff --git a/test/status.test.ts b/test/status.test.ts index c5014eb9..656fcd63 100644 --- a/test/status.test.ts +++ b/test/status.test.ts @@ -1,17 +1,14 @@ import AinftJs from '../src/ainft'; -describe("Status", () => { - it("should return health", async () => { - // TODO(hyeonwoong): remove dev api endpoint. - const config = { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0 - } - const ainftJs = new AinftJs( - 'a'.repeat(64), - config, - ); - expect(await ainftJs.getStatus()).toMatchObject({ health: true }); +describe('status', () => { + it('should return health', async () => { + const ainft = new AinftJs({ + privateKey: 'a'.repeat(64), + baseUrl: 'https://ainft-api-dev.ainetwork.ai', + blockchainUrl: 'https://testnet-api.ainetwork.ai', + chainId: 0, + }); + + expect(await ainft.getStatus()).toMatchObject({ health: true }); }); }); diff --git a/test/test_data.ts b/test/test_data.ts index 63df8b6e..677d7367 100644 --- a/test/test_data.ts +++ b/test/test_data.ts @@ -1,10 +1,11 @@ -export const test_private_key = 'f0a2599e5629d4e67266169ea9ad1999f86995418391175af6d66005c1e1d96c'; -export const test_address = '0x7ed9c30C9F3A31Daa9614b90B4a710f61Bd585c0'; +export const privateKey = 'f0a2599e5629d4e67266169ea9ad1999f86995418391175af6d66005c1e1d96c'; // owner +export const address = '0x7ed9c30C9F3A31Daa9614b90B4a710f61Bd585c0'; -export const test_object_id = '0xB2710D23834d3ef4651dF66661da94C52df38612'; -export const test_app_id = 'ainft721_0xb2710d23834d3ef4651df66661da94c52df38612'; -export const test_token_id = '1'; -export const test_service_name = 'ainize_openai'; -export const test_assistant_id = 'asst_000000000000000000000001'; -export const test_thread_id = 'thread_000000000000000000000001'; -export const test_message_id = 'msg_000000000000000000000001'; \ No newline at end of file +export const objectId = '0xD42cfE651c3ED79F5FE68A7D257d227f1b5282A9'; +export const appId = 'ainft721_0xd42cfe651c3ed79f5fe68a7d257d227f1b5282a9'; +export const tokenId = '7'; +export const serviceName = 'openai_backend'; + +export const assistantId = 'asst_4ZfyVxKP7IG89DzJjSyQTEmv'; +export const threadId = 'thread_8nrYM2UCxnhGTMotlolno9aL'; +export const messageId = 'msg_8s21aQzQZQbPCKsrnFZ3W7hr'; diff --git a/test/threads.test.ts b/test/threads.test.ts deleted file mode 100644 index 7c19a18f..00000000 --- a/test/threads.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import AinftJs from '../src/ainft'; -import { test_private_key, test_object_id, test_token_id, test_thread_id } from './test_data'; - -jest.mock('../src/common/util', () => { - const mockRequest = jest.fn((jobType, body) => { - switch (jobType) { - case 'create_thread': - case 'modify_thread': - return { - ...body, - id: test_thread_id, - created_at: 0, - }; - case 'retrieve_thread': - return { - id: test_thread_id, - metadata: { key1: 'value1' }, - created_at: 0, - }; - case 'delete_thread': - return { - id: test_thread_id, - deleted: true, - }; - default: - return null; - } - }); - const util = jest.requireActual('../src/common/util'); - return { - ...util, - validateAssistant: jest.fn().mockResolvedValue(undefined), - validateThread: jest.fn().mockResolvedValue(undefined), - sendAinizeRequest: mockRequest, - sendTransaction: jest.fn().mockResolvedValue({ - tx_hash: '0x' + 'a'.repeat(64), - result: { code: 0 }, - }), - }; -}); - -const TX_PATTERN = /^0x([A-Fa-f0-9]{64})$/; -const THREAD_PATTERN = /^thread_([A-Za-z0-9]{24})$/; - -jest.setTimeout(60000); - -describe.skip('thread', () => { - let ainft: AinftJs; - - beforeAll(async () => { - ainft = new AinftJs(test_private_key, { - ainftServerEndpoint: 'https://ainft-api-dev.ainetwork.ai', - ainBlockchainEndpoint: 'https://testnet-api.ainetwork.ai', - chainId: 0, - }); - }); - - afterAll(async () => { - jest.restoreAllMocks(); - }); - - it('should create thread', async () => { - const body = { metadata: { key1: 'value1' } }; - - const res = await ainft.chat.thread.create(test_object_id, test_token_id, 'openai', body); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.thread.id).toMatch(THREAD_PATTERN); - expect(res.thread.metadata).toEqual({ key1: 'value1' }); - }); - - it('should get thread', async () => { - const thread = await ainft.chat.thread.get( - test_thread_id, - test_object_id, - test_token_id, - 'openai' - ); - - expect(thread.id).toBe(test_thread_id); - expect(thread.metadata).toEqual({ key1: 'value1' }); - }); - - it('should update thread', async () => { - const body = { metadata: { key1: 'value1', key2: 'value2' } }; - - const res = await ainft.chat.thread.update( - test_thread_id, - test_object_id, - test_token_id, - 'openai', - body - ); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.thread.id).toBe(test_thread_id); - expect(res.thread.metadata).toEqual({ key1: 'value1', key2: 'value2' }); - }); - - it('should delete thread', async () => { - const res = await ainft.chat.thread.delete( - test_thread_id, - test_object_id, - test_token_id, - 'openai' - ); - - expect(res.tx_hash).toMatch(TX_PATTERN); - expect(res.result).toBeDefined(); - expect(res.delThread.id).toBe(test_thread_id); - expect(res.delThread.deleted).toBe(true); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 8b8f2ff3..2c7a38dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "allowUnreachableCode": false, "allowUnusedLabels": false, "declaration": true, + "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, "module": "commonjs", "noEmitOnError": true, diff --git a/yarn.lock b/yarn.lock index 9902d6eb..4bf753fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,12 +2,12 @@ # yarn lockfile v1 -"@ainblockchain/ain-js@^1.10.2": - version "1.10.2" - resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.10.2.tgz#c0ed1e230a6c2b3703169a5c243c173944004bdd" - integrity sha512-2lHYnvbjEHm3/K5Lw5wCNVBtj61Z1p1u+FbveXMg4vDCwUnuYwvCrtZjTxxv13MXHoJ6izdkkA3X93GWtQhpow== +"@ainblockchain/ain-js@^1.13.0": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.13.0.tgz#547a036385ac8ea2fb9a643cbf8151bf4c4b65ed" + integrity sha512-XFxbxamfoUbgh6iOUjIQZPduotEI32lsRZxYCjD5SGlQ/kXzdrZ9rz0EJCFoE0O/Nu6bawC7h96Jvc8M/xon0w== dependencies: - "@ainblockchain/ain-util" "^1.1.9" + "@ainblockchain/ain-util" "^1.2.1" "@types/node" "^12.7.3" "@types/randombytes" "^2.0.0" "@types/semver" "^7.3.4" @@ -31,34 +31,31 @@ uuid "^3.3.3" ws "^8.16.0" -"@ainblockchain/ain-js@^1.6.3": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.6.3.tgz#56ca744a6bf5e558f2acba75f106e8f88f5426ba" - integrity sha512-rdQfT6jcqcF4VP1twwMQkCijZ6SN1RewTjU1D35rJ7ZnRQjoIxekkodkdcIDVvyUEpR6A6iuT9SSSTz9KUMNbA== +"@ainblockchain/ain-util@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@ainblockchain/ain-util/-/ain-util-1.1.9.tgz#4369547af354d84229c5b0f1fd4e93e8497d6227" + integrity sha512-u3q0h0zwWk+vzZ6VpBZiagVKJbNw/Dw4LVjBAhOvgPCx/E3jHHQCufIMDGqD4wjeBuHVtTAQyMTv7LRPSZFBGg== dependencies: - "@ainblockchain/ain-util" "^1.1.9" - "@types/node" "^12.7.3" - "@types/randombytes" "^2.0.0" - "@types/semver" "^7.3.4" - axios "^0.21.4" - bip39 "^3.0.2" + bip39 "^3.0.4" + bn.js "^4.11.8" browserify-cipher "^1.0.1" - eventemitter3 "^4.0.0" - hdkey "^1.1.1" - lodash "^4.17.20" - node-seal "^4.5.7" + eccrypto "^1.1.6" + fast-json-stable-stringify "^2.0.0" + hdkey "^2.0.1" + keccak "^2.0.0" pbkdf2 "^3.0.17" randombytes "^2.1.0" + rlp "^2.2.2" + safe-buffer "^5.1.2" scryptsy "^2.1.0" - semver "^6.3.0" - url-parse "^1.4.7" + secp256k1 "^3.6.2" uuid "^3.3.3" - ws "^8.2.3" + varuint-bitcoin "^1.1.0" -"@ainblockchain/ain-util@^1.1.9": - version "1.1.9" - resolved "https://registry.yarnpkg.com/@ainblockchain/ain-util/-/ain-util-1.1.9.tgz#4369547af354d84229c5b0f1fd4e93e8497d6227" - integrity sha512-u3q0h0zwWk+vzZ6VpBZiagVKJbNw/Dw4LVjBAhOvgPCx/E3jHHQCufIMDGqD4wjeBuHVtTAQyMTv7LRPSZFBGg== +"@ainblockchain/ain-util@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@ainblockchain/ain-util/-/ain-util-1.2.1.tgz#c5445f4718da0820c4955be44b07400172a864ef" + integrity sha512-++Sjv4PBT2/sdHeQNCDNb67ZIObxQnRQzILh+E72BUNnaEcS9cf9+4RC9I8JRqWkBiPvsD7oJPpMYYn0xwfECg== dependencies: bip39 "^3.0.4" bn.js "^4.11.8" @@ -76,12 +73,12 @@ uuid "^3.3.3" varuint-bitcoin "^1.1.0" -"@ainize-team/ainize-js@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@ainize-team/ainize-js/-/ainize-js-1.1.1.tgz#63c675bc63a85f2321283aeb83f01cc023ba1662" - integrity sha512-BS7aKB4Gq0dHk2b5KP30UaJf1QzkexYY28lsy6SB3A/063EA1dXgIG6xcBipxeETCFXSnpPMx9Eh1kUTBakKJg== +"@ainize-team/ainize-js@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@ainize-team/ainize-js/-/ainize-js-1.3.1.tgz#063e1a59c22bdfbcaca15875d956e70c6740820c" + integrity sha512-ihzqaeAroVygFiMA7LiL5/RMD5eNltjnBEbyvFUf5Lk6i/lVPg/L6wJOMMUrMTA1DCcZ+5oNhROofWmw5h6+4Q== dependencies: - "@ainblockchain/ain-js" "^1.6.3" + "@ainblockchain/ain-js" "^1.13.0" axios "^0.26.1" express "^4.18.2" fast-json-stable-stringify "^2.1.0" @@ -2958,15 +2955,7 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -3935,14 +3924,9 @@ write-file-atomic@^4.0.2: signal-exit "^3.0.7" ws@^8.16.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -ws@^8.2.3: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" - integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== y18n@^5.0.5: version "5.0.8" @@ -3960,9 +3944,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.4.5" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" - integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1"