diff --git a/README.md b/README.md index ef89d4b..7af2da4 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,37 @@ const mintedNfd = await nfd }) ``` +### Purchasing NFDs (Claiming & Buying) + +```typescript +import { NfdClient } from '@txnlab/nfd-sdk' + +const nfd = NfdClient.testNet() + +// Get a purchase quote to check eligibility and pricing +const quote = await nfd + .setSigner(activeAddress, transactionSigner) + .getPurchaseQuote('reserved-nfd.algo') + +if (quote.canClaim) { + // Claim a reserved NFD (automatically uses signer's address) + const claimedNfd = await nfd + .setSigner(activeAddress, transactionSigner) + .claim('reserved-nfd.algo') + + console.log('Successfully claimed:', claimedNfd.name) +} + +if (quote.canBuy) { + // Buy an NFD from the secondary market + const purchasedNfd = await nfd + .setSigner(activeAddress, transactionSigner) + .buy('forsale-nfd.algo') + + console.log('Successfully purchased:', purchasedNfd.name) +} +``` + ### Managing an NFD ```typescript diff --git a/examples/README.md b/examples/README.md index c304e0b..2461fe0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,6 +8,7 @@ This directory contains example applications demonstrating various features of t - [API Search](./api-search/): Demonstrates how to use the API client to search for NFDs - [Reverse Lookup](./reverse-lookup/): Demonstrates how to look up NFDs by wallet address - [Mint](./mint/): Demonstrates how to mint NFDs +- [Claim NFD](./claim-nfd/): Demonstrates how to claim NFDs reserved for your wallet address - [Link Address](./link-address/): Demonstrates how to link addresses to NFDs - [Set Metadata](./set-metadata/): Demonstrates how to set metadata for NFDs - [Set Primary NFD](./set-primary-nfd/): Demonstrates how to set a primary NFD for an address diff --git a/examples/claim-nfd/.gitignore b/examples/claim-nfd/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/examples/claim-nfd/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/claim-nfd/README.md b/examples/claim-nfd/README.md new file mode 100644 index 0000000..fe8fb41 --- /dev/null +++ b/examples/claim-nfd/README.md @@ -0,0 +1,60 @@ +# NFD Claim Example + +This example demonstrates how to use the NFD SDK to claim NFDs that are reserved for your wallet address. + +## Features + +- Connect your wallet using the Wallet Connect protocol +- Search for NFDs reserved for your address using the NFD API +- Claim reserved NFDs using the NFD SDK's claiming API +- Simple and clean UI for managing the claiming process + +## Getting Started + +1. Install dependencies: + +```bash +npm install +``` + +2. Start the development server: + +```bash +npm run dev +``` + +3. Open your browser to the provided localhost URL + +## How it Works + +1. **Connect Wallet**: Use the wallet connection interface to connect your Algorand wallet +2. **Search Reserved NFDs**: The app automatically searches for NFDs reserved for your connected address +3. **Claim NFDs**: Click the "Claim" button next to any reserved NFD to claim it to your wallet + +## Code Example + +Here's how simple it is to claim an NFD with the new API: + +```typescript +// Simple claiming - just connect wallet and call claim() +const claimedNfd = await nfd + .setSigner(address, transactionSigner) + .claim('reserved-nfd.algo') +``` + +The NFD SDK automatically uses your connected wallet address as the claimer, making the API clean and intuitive. + +## Important Notes + +- This example runs on Algorand TestNet +- You need TestNet ALGO in your wallet to pay for transaction fees +- Reserved NFDs may have a claiming cost (calculated based on the NFD's pricing), plus transaction fees +- Make sure your wallet is connected to TestNet + +## Technology Stack + +- **React**: Frontend framework +- **TypeScript**: Type safety +- **Vite**: Build tool and dev server +- **@txnlab/nfd-sdk**: NFD SDK for blockchain interactions +- **@txnlab/use-wallet-react**: Wallet connection management diff --git a/examples/claim-nfd/eslint.config.js b/examples/claim-nfd/eslint.config.js new file mode 100644 index 0000000..37269aa --- /dev/null +++ b/examples/claim-nfd/eslint.config.js @@ -0,0 +1,42 @@ +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import globals from 'globals' +import tseslint from 'typescript-eslint' +import baseConfig from '../../eslint.config.js' + +export default tseslint.config( + ...baseConfig, + { + files: ['src/**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + project: './tsconfig.json', + }, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, + { + files: ['*.config.{js,ts}'], + languageOptions: { + parserOptions: { + project: './tsconfig.node.json', + }, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + }, + }, +) diff --git a/examples/claim-nfd/index.html b/examples/claim-nfd/index.html new file mode 100644 index 0000000..b5f7178 --- /dev/null +++ b/examples/claim-nfd/index.html @@ -0,0 +1,13 @@ + + + + + + + NFD Claim Example + + +
+ + + diff --git a/examples/claim-nfd/package.json b/examples/claim-nfd/package.json new file mode 100644 index 0000000..68e4aaa --- /dev/null +++ b/examples/claim-nfd/package.json @@ -0,0 +1,27 @@ +{ + "name": "@txnlab/nfd-sdk-claim-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"", + "preview": "vite preview" + }, + "dependencies": { + "@algorandfoundation/algokit-utils": "^8.2.2", + "@txnlab/nfd-sdk": "^0.8.0", + "@txnlab/use-wallet-react": "^4.0.0", + "algosdk": "^3.2.0", + "lute-connect": "^1.4.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.64", + "@types/react-dom": "^18.2.21", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^4.5.2", + "vite-plugin-node-polyfills": "^0.23.0" + } +} diff --git a/examples/claim-nfd/public/vite.svg b/examples/claim-nfd/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/claim-nfd/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/claim-nfd/src/App.tsx b/examples/claim-nfd/src/App.tsx new file mode 100644 index 0000000..5291941 --- /dev/null +++ b/examples/claim-nfd/src/App.tsx @@ -0,0 +1,279 @@ +import { NfdClient, type Nfd, type SearchResponse } from '@txnlab/nfd-sdk' +import { useWallet } from '@txnlab/use-wallet-react' +import { useState, useEffect, useCallback } from 'react' + +import { WalletMenu } from './Connect' + +/** + * Initialize the NFD client for TestNet + */ +const nfd = NfdClient.testNet() + +export function App() { + const [reservedNfds, setReservedNfds] = useState([]) + const [error, setError] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [claimingNfd, setClaimingNfd] = useState(null) + const [claimedNfds, setClaimedNfds] = useState>(new Set()) + + const { activeAddress, transactionSigner } = useWallet() + + /** + * Search for NFDs reserved for the active address + */ + const searchReservedNfds = useCallback(async () => { + if (!activeAddress) return + + setIsLoading(true) + setError('') + + try { + // Search for NFDs reserved for the active address + const searchResults: SearchResponse = await nfd.api.search({ + reservedFor: activeAddress, + view: 'full', + limit: 100, // Get up to 100 reserved NFDs + }) + + console.log('searchResults', searchResults) + setReservedNfds(searchResults.nfds) + } catch (err) { + console.error('Error searching for reserved NFDs:', err) + setError('Failed to load reserved NFDs. Please try again.') + } finally { + setIsLoading(false) + } + }, [activeAddress]) + + /** + * Claim a reserved NFD + */ + const claimNfd = async (nfdName: string) => { + if (!activeAddress || !transactionSigner) { + setError('Please connect your wallet first') + return + } + + setClaimingNfd(nfdName) + setError('') + + try { + // Use the simplified API to claim the NFD directly + const claimedNfd = await nfd + .setSigner(activeAddress, transactionSigner) + .claim(nfdName) + + console.log('Successfully claimed NFD:', claimedNfd) + + // Add to claimed NFDs set + setClaimedNfds((prev) => new Set(prev).add(nfdName)) + + // Refresh the list of reserved NFDs + await searchReservedNfds() + } catch (err) { + console.error('Error claiming NFD:', err) + setError( + `Failed to claim ${nfdName}. ${err instanceof Error ? err.message : 'Please try again.'}`, + ) + } finally { + setClaimingNfd(null) + } + } + + // Load reserved NFDs when wallet connects + useEffect(() => { + if (activeAddress) { + searchReservedNfds() + } else { + setReservedNfds([]) + setError('') + setClaimedNfds(new Set()) + } + }, [activeAddress, searchReservedNfds]) + + return ( +
+

NFD Claim Tool

+

Claim NFDs that are reserved for your wallet address

+ +
+ +
+ + {error && ( +
+ {error} +
+ )} + + {activeAddress && ( +
+
+

Connected Address

+ + {activeAddress} + +
+ +
+ +
+ +
+

Reserved NFDs ({reservedNfds.length})

+ + {isLoading && ( +

+ Searching for NFDs reserved for your address... +

+ )} + + {!isLoading && reservedNfds.length === 0 && ( +

+ No NFDs are currently reserved for your address. +

+ )} + + {reservedNfds.length > 0 && ( +
+ {reservedNfds.map((nfdData) => ( +
+
+
+

+ {nfdData.name} +

+

+ State: {nfdData.state} +

+ {nfdData.reservedFor && ( +

+ Reserved for: {nfdData.reservedFor} +

+ )} + {claimedNfds.has(nfdData.name) && ( +

+ ✓ Successfully claimed! +

+ )} +
+ +
+ {!claimedNfds.has(nfdData.name) && ( + + )} +
+
+
+ ))} +
+ )} +
+
+ )} + + {!activeAddress && ( +
+

+ Please connect your wallet to search for and claim reserved NFDs. +

+
+ )} +
+ ) +} diff --git a/examples/claim-nfd/src/Connect.tsx b/examples/claim-nfd/src/Connect.tsx new file mode 100644 index 0000000..cf13821 --- /dev/null +++ b/examples/claim-nfd/src/Connect.tsx @@ -0,0 +1,103 @@ +import { useWallet, type Wallet } from '@txnlab/use-wallet-react' +import { useState } from 'react' + +export const WalletMenu = () => { + const { wallets, activeWallet } = useWallet() + + if (activeWallet) { + return + } + + return +} + +const WalletList = ({ wallets }: { wallets: Wallet[] }) => { + return ( +
+

Connect Wallet

+
+ {wallets.map((wallet) => ( + + ))} +
+
+ ) +} + +const WalletOption = ({ wallet }: { wallet: Wallet }) => { + const [connecting, setConnecting] = useState(false) + + const handleConnect = async () => { + setConnecting(true) + try { + await wallet.connect() + } catch (error) { + console.error('Failed to connect:', error) + } finally { + setConnecting(false) + } + } + + return ( + + ) +} + +const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { + return ( +
+
+ {wallet.metadata.name} + {wallet.metadata.name} +
+ + {wallet.accounts.length > 1 && ( + + )} + + {wallet.activeAccount && ( +
+ Active Account: {wallet.activeAccount.address} +
+ )} + +
+ +
+
+ ) +} diff --git a/examples/claim-nfd/src/index.tsx b/examples/claim-nfd/src/index.tsx new file mode 100644 index 0000000..377116a --- /dev/null +++ b/examples/claim-nfd/src/index.tsx @@ -0,0 +1,30 @@ +import { + WalletId, + WalletManager, + WalletProvider, +} from '@txnlab/use-wallet-react' +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' + +import { App } from './App' + +const walletManager = new WalletManager({ + wallets: [ + { + id: WalletId.LUTE, + options: { siteName: 'NFD SDK Mint Example' }, + }, + ], + defaultNetwork: 'testnet', + options: { + resetNetwork: true, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/examples/claim-nfd/tsconfig.app.json b/examples/claim-nfd/tsconfig.app.json new file mode 100644 index 0000000..358ca9b --- /dev/null +++ b/examples/claim-nfd/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/claim-nfd/tsconfig.json b/examples/claim-nfd/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/examples/claim-nfd/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/claim-nfd/tsconfig.node.json b/examples/claim-nfd/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/examples/claim-nfd/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/claim-nfd/vite.config.ts b/examples/claim-nfd/vite.config.ts new file mode 100644 index 0000000..e42dc7c --- /dev/null +++ b/examples/claim-nfd/vite.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import { nodePolyfills } from 'vite-plugin-node-polyfills' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + nodePolyfills({ + globals: { + Buffer: true, + }, + }), + ], +}) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 1108bdc..a5773c7 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -110,6 +110,37 @@ const mintedNfd = await nfd }) ``` +### Purchasing NFDs (Claiming & Buying) + +```typescript +import { NfdClient } from '@txnlab/nfd-sdk' + +const nfd = NfdClient.testNet() + +// Get a purchase quote to check eligibility and pricing +const quote = await nfd + .setSigner(activeAddress, transactionSigner) + .getPurchaseQuote('reserved-nfd.algo') + +if (quote.canClaim) { + // Claim a reserved NFD (automatically uses signer's address) + const claimedNfd = await nfd + .setSigner(activeAddress, transactionSigner) + .claim('reserved-nfd.algo') + + console.log('Successfully claimed:', claimedNfd.name) +} + +if (quote.canBuy) { + // Buy an NFD from the secondary market + const purchasedNfd = await nfd + .setSigner(activeAddress, transactionSigner) + .buy('forsale-nfd.algo') + + console.log('Successfully purchased:', purchasedNfd.name) +} +``` + ### Managing an NFD ```typescript diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index c458537..2be97fa 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -12,6 +12,7 @@ import { NfdMintQuote, NfdMintQuoteParams, } from './modules/minting' +import { PurchasingModule, NfdPurchaseQuote } from './modules/purchasing' import type { Nfd, @@ -46,6 +47,7 @@ export class NfdClient { // Core modules private readonly _lookup: LookupModule private readonly _minting: MintingModule + private readonly _purchasing: PurchasingModule private _signer: TransactionSignerAccount | null = null @@ -57,6 +59,7 @@ export class NfdClient { // Initialize modules this._lookup = new LookupModule(this) this._minting = new MintingModule(this) + this._purchasing = new PurchasingModule(this) } /** @@ -138,6 +141,14 @@ export class NfdClient { return new NfdManager(this, nameOrAppId) } + /** + * Get access to purchasing and claiming functionality + * @returns A purchasing module instance + */ + public purchasing(): PurchasingModule { + return new PurchasingModule(this) + } + /** * Resolve an NFD by name or application ID by reading directly from the blockchain * @param nameOrAppId - The NFD name or application ID to resolve @@ -182,6 +193,63 @@ export class NfdClient { } } + /** + * Get a quote for purchasing an NFD + * @param nameOrAppId - The NFD name or application ID to get a quote for + * @returns A detailed purchase quote including price and eligibility + * @throws If the quote cannot be generated or signer is not set + */ + public async getPurchaseQuote( + nameOrAppId: string | number | bigint, + ): Promise { + if (!this._signer) { + throw new Error('Signer must be set before getting purchase quote') + } + + return this._purchasing.getPurchaseQuote( + nameOrAppId, + this._signer.addr.toString(), + ) + } + + /** + * Claim an NFD that is reserved for the claimer + * @param nameOrAppId - The NFD name or application ID to claim + * @returns The claimed NFD record + * @throws If the claim operation fails or signer is not set + */ + public async claim(nameOrAppId: string | number | bigint): Promise { + if (!this._signer) { + throw new Error('Signer must be set before claiming NFD') + } + + try { + return await this._purchasing.claim(nameOrAppId) + } finally { + // Reset signer after operation + this._signer = null + } + } + + /** + * Buy an NFD from the secondary market + * @param nameOrAppId - The NFD name or application ID to buy + * @returns The purchased NFD record + * @throws If the buy operation fails or signer is not set + */ + public async buy(nameOrAppId: string | number | bigint): Promise { + if (!this._signer) { + throw new Error('Signer must be set before buying NFD') + } + + try { + return await this._purchasing.buy(nameOrAppId) + } finally { + // Reset signer after operation + this._signer = null + } + } + /** * Resolve an address to find its associated NFD * @param address - The address to resolve diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 2455080..3df704b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -18,6 +18,7 @@ export { NfdApiClient } from './api-client' // Export modules export { NfdManager } from './modules/manager' +export { PurchasingModule } from './modules/purchasing' // Export module types export type { @@ -26,6 +27,8 @@ export type { NfdMintParams, } from './modules/minting' +export type { NfdPurchaseQuote } from './modules/purchasing' + // Export NFD utility functions export { isValidName, diff --git a/packages/sdk/src/modules/purchasing.ts b/packages/sdk/src/modules/purchasing.ts new file mode 100644 index 0000000..737711e --- /dev/null +++ b/packages/sdk/src/modules/purchasing.ts @@ -0,0 +1,295 @@ +import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' +import { Address } from 'algosdk' + +import { Nfd } from '../types' +import { parseTransactionError } from '../utils/error-parser' + +import { BaseModule } from './base' + +/** + * Response structure for purchase quote requests + */ +export interface NfdPurchaseQuote { + /** The NFD name being quoted */ + nfdName: string + /** The address of the buyer */ + buyer: string + /** Whether this NFD can be claimed (is reserved for the buyer) */ + canClaim: boolean + /** Whether this NFD can be bought (is for sale) */ + canBuy: boolean + /** The price to purchase in microAlgos (calculated amount for claims, sellAmount for purchases) */ + price: bigint + /** The address the NFD is reserved for (if any) */ + reservedFor?: string + /** The current sell amount in microAlgos (if for sale) */ + sellAmount?: bigint + /** The current state of the NFD */ + state: string + /** Whether the buyer is authorized to make this purchase */ + authorized: boolean + /** Reason why purchase is not authorized (if applicable) */ + authorizationError?: string +} + +/** + * Module for handling NFD purchasing operations (claiming and buying) + */ +export class PurchasingModule extends BaseModule { + /** + * Validate an Algorand address + * @private + */ + private validateAddress(address: string, paramName: string): void { + try { + Address.fromString(address) + } catch { + throw new Error(`Invalid ${paramName}: ${address}`) + } + } + + /** + * Get a purchase quote for an NFD + * @param nameOrAppId - The NFD name or application ID + * @param buyer - The buyer address + * @returns Detailed purchase quote including eligibility and pricing + * @throws If the NFD cannot be resolved or quote cannot be generated + */ + public async getPurchaseQuote( + nameOrAppId: string | number | bigint, + buyer: string, + ): Promise { + // Validate buyer address + this.validateAddress(buyer, 'buyer') + + // Resolve NFD to get current information + const nfd = await this.client.resolve(nameOrAppId, { view: 'full' }) + + // Check if buyer can claim this NFD (is reserved for them) + // Check all possible locations for reservation info + const reservedForAddress = + nfd.reservedFor || + nfd.properties?.internal?.reservedOwner || + nfd.properties?.verified?.reservedFor + const isReservedForBuyer = reservedForAddress === buyer + + // NFD can be claimed if it's in 'reserved' state OR if it's 'forSale' but reserved for the buyer + const canClaim = + (nfd.state === 'reserved' || nfd.state === 'forSale') && + isReservedForBuyer + + // Check if buyer can buy this NFD (is for sale and not specifically reserved) + const canBuy = nfd.state === 'forSale' && !reservedForAddress + + let authorized = false + let authorizationError: string | undefined + + // Determine authorization + if (nfd.state === 'reserved' || nfd.state === 'forSale') { + if (reservedForAddress && !isReservedForBuyer) { + authorized = false + authorizationError = `NFD is reserved for ${reservedForAddress}, but buyer is ${buyer}` + } else if (canClaim || canBuy) { + authorized = true + } else { + authorized = false + authorizationError = `NFD is not available for purchase (state: ${nfd.state})` + } + } else { + authorized = false + authorizationError = `NFD is not available for purchase (state: ${nfd.state})` + } + + // Determine price + let price = BigInt(0) + if (canClaim && nfd.sellAmount) { + // For claims, calculate the amount based on sellAmount minus mintingKickoffAmount + const sellAmount = Number(nfd.sellAmount) + const mintingKickoffAmount = + Number(nfd.properties?.internal?.mintingKickoffAmount) || 0 + const claimAmount = Math.max(sellAmount - mintingKickoffAmount, 0) + price = BigInt(claimAmount) + } else if (canBuy && nfd.sellAmount) { + price = BigInt(nfd.sellAmount) + } + + return { + nfdName: nfd.name, + buyer, + canClaim, + canBuy, + price, + reservedFor: reservedForAddress, + sellAmount: nfd.sellAmount ? BigInt(nfd.sellAmount) : undefined, + state: nfd.state || 'unknown', + authorized, + authorizationError, + } + } + + /** + * Claim an NFD that is reserved for the caller + * @param nameOrAppId - The NFD name or application ID to claim + * @returns The claimed NFD record + * @throws If the claim operation fails + */ + public async claim(nameOrAppId: string | number | bigint): Promise { + // Ensure a signer is set + const signer = this.requireSigner() + + const claimer = signer.addr.toString() + + // Validate claimer address + this.validateAddress(claimer, 'claimer') + + // Get purchase quote to validate eligibility + const quote = await this.getPurchaseQuote(nameOrAppId, claimer) + + if (!quote.canClaim) { + throw new Error( + quote.authorizationError || + `Cannot claim NFD: ${quote.nfdName} (state: ${quote.state})`, + ) + } + + if (!quote.authorized) { + throw new Error( + quote.authorizationError || + `Not authorized to claim NFD: ${quote.nfdName}`, + ) + } + + // Resolve NFD to get app ID + const nfd = await this.client.resolve(nameOrAppId, { view: 'full' }) + if (!nfd.appID) { + throw new Error(`Cannot determine app ID for NFD: ${nfd.name}`) + } + + // Get the NFD instance client + const nfdInstanceClient = this.getInstanceClient(BigInt(nfd.appID), claimer) + + try { + // Create payment transaction with calculated claim amount + const paymentTxn = await this.algorand.createTransaction.payment({ + sender: claimer, + receiver: nfdInstanceClient.appAddress, + amount: AlgoAmount.MicroAlgos(quote.price), + }) + + // Execute the purchase (claim) transaction using transaction group pattern + await nfdInstanceClient + .newGroup() + .purchase({ + args: { payment: paymentTxn }, + staticFee: AlgoAmount.MicroAlgos(4000), // 0.004 ALGO + }) + .send({ populateAppCallResources: true }) + + // Return the updated NFD record + return this.client.resolve(nfd.appID, { view: 'full' }) + } catch (error) { + throw new Error(`Failed to claim NFD: ${parseTransactionError(error)}`) + } + } + + /** + * Buy an NFD from the secondary market + * @param nameOrAppId - The NFD name or application ID to buy + * @returns The purchased NFD record + * @throws If the buy operation fails + */ + public async buy(nameOrAppId: string | number | bigint): Promise { + // Ensure a signer is set + const signer = this.requireSigner() + + const buyer = signer.addr.toString() + + // Validate buyer address + this.validateAddress(buyer, 'buyer') + + // Get purchase quote to validate eligibility and get pricing + const quote = await this.getPurchaseQuote(nameOrAppId, buyer) + + if (!quote.canBuy) { + throw new Error( + quote.authorizationError || + `Cannot buy NFD: ${quote.nfdName} (state: ${quote.state})`, + ) + } + + if (!quote.authorized) { + throw new Error( + quote.authorizationError || + `Not authorized to buy NFD: ${quote.nfdName}`, + ) + } + + // Resolve NFD to get app ID + const nfd = await this.client.resolve(nameOrAppId, { view: 'full' }) + if (!nfd.appID) { + throw new Error(`Cannot determine app ID for NFD: ${nfd.name}`) + } + + // Get the NFD instance client + const nfdInstanceClient = this.getInstanceClient(BigInt(nfd.appID), buyer) + + try { + // Create payment transaction for the purchase amount + const paymentTxn = await this.algorand.createTransaction.payment({ + sender: buyer, + receiver: nfdInstanceClient.appAddress, + amount: AlgoAmount.MicroAlgos(quote.price), + }) + + // Execute the purchase transaction using transaction group pattern + await nfdInstanceClient + .newGroup() + .purchase({ + args: { payment: paymentTxn }, + staticFee: AlgoAmount.MicroAlgos(4000), // 0.004 ALGO + }) + .send({ populateAppCallResources: true }) + + // Return the updated NFD record + return this.client.resolve(nfd.appID, { view: 'full' }) + } catch (error) { + throw new Error(`Failed to buy NFD: ${parseTransactionError(error)}`) + } + } + + /** + * Check if an NFD can be claimed by a specific address + * @param nameOrAppId - The NFD name or application ID + * @param claimer - The address to check claim eligibility for + * @returns True if the NFD can be claimed, false otherwise + */ + public async canClaim( + nameOrAppId: string | number | bigint, + claimer: string, + ): Promise { + try { + const quote = await this.getPurchaseQuote(nameOrAppId, claimer) + return quote.canClaim && quote.authorized + } catch { + return false + } + } + + /** + * Check if an NFD can be bought by a specific address + * @param nameOrAppId - The NFD name or application ID + * @param buyer - The address to check buy eligibility for + * @returns True if the NFD can be bought, false otherwise + */ + public async canBuy( + nameOrAppId: string | number | bigint, + buyer: string, + ): Promise { + try { + const quote = await this.getPurchaseQuote(nameOrAppId, buyer) + return quote.canBuy && quote.authorized + } catch { + return false + } + } +} diff --git a/packages/sdk/tests/client.test.ts b/packages/sdk/tests/client.test.ts index 3c051d0..2d18254 100644 --- a/packages/sdk/tests/client.test.ts +++ b/packages/sdk/tests/client.test.ts @@ -1,5 +1,6 @@ import { AlgorandClient } from '@algorandfoundation/algokit-utils' -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { TransactionSigner } from 'algosdk' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { NfdClient } from '../src/client' import { NfdRegistryId } from '../src/constants' @@ -50,12 +51,43 @@ vi.mock('../src/modules/minting', () => ({ })), })) +vi.mock('../src/modules/purchasing', () => ({ + PurchasingModule: vi.fn().mockImplementation(() => ({ + getPurchaseQuote: vi.fn().mockResolvedValue({ + nfdName: 'test.algo', + buyer: VALID_ADDRESS, + canClaim: true, + canBuy: false, + price: BigInt(0), + reservedFor: VALID_ADDRESS, + sellAmount: BigInt(1000000), + state: 'reserved' as const, + authorized: true, + authorizationError: undefined, + }), + claim: vi.fn().mockResolvedValue({ + name: 'test.algo', + appID: 12345, + state: 'owned' as const, + owner: VALID_ADDRESS, + }), + buy: vi.fn().mockResolvedValue({ + name: 'test.algo', + appID: 12345, + state: 'owned' as const, + owner: VALID_ADDRESS, + }), + })), +})) + describe('NfdClient', () => { let client: NfdClient + let mockSigner: TransactionSigner beforeEach(() => { vi.clearAllMocks() client = new NfdClient() + mockSigner = vi.fn() }) describe('constructor', () => { @@ -92,7 +124,6 @@ describe('NfdClient', () => { describe('setSigner', () => { it('should set the signer and return the client for chaining', () => { - const mockSigner = vi.fn() const result = client.setSigner(VALID_ADDRESS, mockSigner) expect(result).toBe(client) expect(client.signer).not.toBeNull() @@ -119,7 +150,6 @@ describe('NfdClient', () => { describe('mint', () => { it('should call the minting module mint method and reset signer', async () => { // Set a signer first - const mockSigner = vi.fn() client.setSigner(VALID_ADDRESS, mockSigner) expect(client.signer).not.toBeNull() @@ -134,4 +164,75 @@ describe('NfdClient', () => { expect(client.signer).toBeNull() }) }) + + describe('Purchasing methods', () => { + beforeEach(() => { + client.setSigner(VALID_ADDRESS, mockSigner) + }) + + it('should delegate getPurchaseQuote to purchasing module', async () => { + const result = await client.getPurchaseQuote('test.algo') + + expect(result).toEqual({ + nfdName: 'test.algo', + buyer: VALID_ADDRESS, + canClaim: true, + canBuy: false, + price: BigInt(0), + reservedFor: VALID_ADDRESS, + sellAmount: BigInt(1000000), + state: 'reserved', + authorized: true, + authorizationError: undefined, + }) + }) + + it('should delegate claim to purchasing module and reset signer', async () => { + const result = await client.claim('test.algo') + + expect(result).toEqual({ + name: 'test.algo', + appID: 12345, + state: 'owned', + owner: VALID_ADDRESS, + }) + expect(client.signer).toBeNull() // Should reset signer after operation + }) + + it('should delegate buy to purchasing module and reset signer', async () => { + const result = await client.buy('test.algo') + + expect(result).toEqual({ + name: 'test.algo', + appID: 12345, + state: 'owned', + owner: VALID_ADDRESS, + }) + expect(client.signer).toBeNull() // Should reset signer after operation + }) + + it('should throw error if signer not set for getPurchaseQuote', async () => { + const clientWithoutSigner = NfdClient.testNet() + + await expect( + clientWithoutSigner.getPurchaseQuote('test.algo'), + ).rejects.toThrow('Signer must be set before getting purchase quote') + }) + + it('should throw error if signer not set for claim', async () => { + const clientWithoutSigner = NfdClient.testNet() + + await expect(clientWithoutSigner.claim('test.algo')).rejects.toThrow( + 'Signer must be set before claiming NFD', + ) + }) + + it('should throw error if signer not set for buy', async () => { + const clientWithoutSigner = NfdClient.testNet() + + await expect(clientWithoutSigner.buy('test.algo')).rejects.toThrow( + 'Signer must be set before buying NFD', + ) + }) + }) }) diff --git a/packages/sdk/tests/purchasing.test.ts b/packages/sdk/tests/purchasing.test.ts new file mode 100644 index 0000000..f6ae3fb --- /dev/null +++ b/packages/sdk/tests/purchasing.test.ts @@ -0,0 +1,472 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +import { NfdClient } from '../src/client' +import { PurchasingModule } from '../src/modules/purchasing' + +import type { Nfd } from '../src/types' + +// Valid Algorand addresses for testing +const BUYER_ADDRESS = + 'ZZAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM' +const SELLER_ADDRESS = + 'BBAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM' +const OTHER_ADDRESS = + 'CCAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM' + +// Mock NFD data for different scenarios +const mockReservedNfd: Nfd = { + name: 'reserved.algo', + appID: 123, + state: 'reserved', + reservedFor: BUYER_ADDRESS, + owner: 'some-nfd-account', + sellAmount: 500000, // 0.5 ALGO + properties: { + internal: { + mintingKickoffAmount: '100000', // 0.1 ALGO + }, + }, +} + +const mockForSaleNfd: Nfd = { + name: 'forsale.algo', + appID: 456, + state: 'forSale', + sellAmount: 1000000, // 1 ALGO + owner: SELLER_ADDRESS, +} + +const mockForSaleReservedNfd: Nfd = { + name: 'forsale-reserved.algo', + appID: 789, + state: 'forSale', + sellAmount: 2000000, // 2 ALGO + reservedFor: BUYER_ADDRESS, + owner: SELLER_ADDRESS, + properties: { + internal: { + mintingKickoffAmount: '500000', // 0.5 ALGO + }, + }, +} + +const mockOwnedNfd: Nfd = { + name: 'owned.algo', + appID: 321, + state: 'owned', + owner: OTHER_ADDRESS, +} + +// Mock types +interface MockAlgorand { + createTransaction: { + payment: ReturnType + } + setSigner: ReturnType +} + +interface MockInstanceClient { + appAddress: string + newGroup: ReturnType +} + +interface MockSigner { + addr: { toString: () => string } + signer: ReturnType +} + +// Mock Algorand client +const mockAlgorand: MockAlgorand = { + createTransaction: { + payment: vi.fn().mockResolvedValue({ id: 'mock-txn' }), + }, + setSigner: vi.fn(), +} + +// Mock NFD instance client +const mockInstanceClient: MockInstanceClient = { + appAddress: 'mock-app-address', + newGroup: vi.fn().mockReturnValue({ + purchase: vi.fn().mockReturnValue({ + send: vi.fn().mockResolvedValue({}), + }), + }), +} + +// Mock the dependencies +vi.mock('algosdk', () => ({ + isValidAddress: vi.fn((addr: string) => addr.length === 58), + Address: { + fromString: vi.fn((addr: string) => { + // Throw error for invalid addresses (less than 58 characters) + if (addr.length !== 58) { + throw new Error('Invalid address') + } + return { + toString: () => addr, + publicKey: new Uint8Array(32), + } + }), + }, +})) + +vi.mock('@algorandfoundation/algokit-utils', () => ({ + AlgorandClient: { + mainNet: vi.fn(() => mockAlgorand), + }, + AlgoAmount: { + MicroAlgos: vi.fn((amount) => ({ + amountInMicroAlgo: typeof amount === 'bigint' ? amount : BigInt(amount), + microAlgos: typeof amount === 'bigint' ? amount : BigInt(amount), + })), + }, +})) + +vi.mock('../src/utils/error-parser', () => ({ + parseTransactionError: vi.fn((error) => error.message || 'Unknown error'), +})) + +describe('PurchasingModule', () => { + let client: NfdClient + let purchasing: PurchasingModule + let mockSigner: MockSigner + + beforeEach(() => { + vi.clearAllMocks() + + // Create mock signer + mockSigner = { + addr: { toString: () => BUYER_ADDRESS }, + signer: vi.fn(), + } + + // Create client and purchasing module + client = new NfdClient() + client.setSigner(BUYER_ADDRESS, mockSigner.signer) + purchasing = new PurchasingModule(client) + + // Mock client methods + vi.spyOn(client, 'resolve').mockImplementation(async (nameOrAppId) => { + if (nameOrAppId === 'reserved.algo' || nameOrAppId === 123) { + return mockReservedNfd + } + if (nameOrAppId === 'forsale.algo' || nameOrAppId === 456) { + return mockForSaleNfd + } + if (nameOrAppId === 'forsale-reserved.algo' || nameOrAppId === 789) { + return mockForSaleReservedNfd + } + if (nameOrAppId === 'owned.algo' || nameOrAppId === 321) { + return mockOwnedNfd + } + throw new Error('NFD not found') + }) + + // Mock getInstanceClient method + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(purchasing as any, 'getInstanceClient').mockReturnValue( + mockInstanceClient, + ) + }) + + describe('getPurchaseQuote', () => { + it('should return a valid quote for a reserved NFD', async () => { + const quote = await purchasing.getPurchaseQuote( + 'reserved.algo', + BUYER_ADDRESS, + ) + + expect(quote).toEqual({ + nfdName: 'reserved.algo', + buyer: BUYER_ADDRESS, + canClaim: true, + canBuy: false, + price: BigInt(400000), // sellAmount (500000) - mintingKickoffAmount (100000) + reservedFor: BUYER_ADDRESS, + sellAmount: BigInt(500000), + state: 'reserved', + authorized: true, + authorizationError: undefined, + }) + }) + + it('should return a valid quote for an NFD for sale', async () => { + const quote = await purchasing.getPurchaseQuote( + 'forsale.algo', + BUYER_ADDRESS, + ) + + expect(quote).toEqual({ + nfdName: 'forsale.algo', + buyer: BUYER_ADDRESS, + canClaim: false, + canBuy: true, + price: BigInt(1000000), + reservedFor: undefined, + sellAmount: BigInt(1000000), + state: 'forSale', + authorized: true, + authorizationError: undefined, + }) + }) + + it('should return unauthorized for NFD reserved for someone else', async () => { + const quote = await purchasing.getPurchaseQuote( + 'reserved.algo', + OTHER_ADDRESS, + ) + + expect(quote).toEqual({ + nfdName: 'reserved.algo', + buyer: OTHER_ADDRESS, + canClaim: false, + canBuy: false, + price: BigInt(0), // No price calculated since not authorized + reservedFor: BUYER_ADDRESS, + sellAmount: BigInt(500000), + state: 'reserved', + authorized: false, + authorizationError: `NFD is reserved for ${BUYER_ADDRESS}, but buyer is ${OTHER_ADDRESS}`, + }) + }) + + it('should return unauthorized for owned NFD', async () => { + const quote = await purchasing.getPurchaseQuote( + 'owned.algo', + BUYER_ADDRESS, + ) + + expect(quote).toEqual({ + nfdName: 'owned.algo', + buyer: BUYER_ADDRESS, + canClaim: false, + canBuy: false, + price: BigInt(0), + reservedFor: undefined, + sellAmount: undefined, + state: 'owned', + authorized: false, + authorizationError: 'NFD is not available for purchase (state: owned)', + }) + }) + + it('should return a valid quote for an NFD for sale but reserved for buyer', async () => { + const quote = await purchasing.getPurchaseQuote( + 'forsale-reserved.algo', + BUYER_ADDRESS, + ) + + expect(quote).toEqual({ + nfdName: 'forsale-reserved.algo', + buyer: BUYER_ADDRESS, + canClaim: true, // Should be claimable since it's reserved for the buyer + canBuy: false, // Cannot buy since it's reserved + price: BigInt(1500000), // sellAmount (2000000) - mintingKickoffAmount (500000) + reservedFor: BUYER_ADDRESS, + sellAmount: BigInt(2000000), + state: 'forSale', + authorized: true, + authorizationError: undefined, + }) + }) + + it('should throw error for invalid buyer address', async () => { + await expect( + purchasing.getPurchaseQuote('reserved.algo', 'invalid-address'), + ).rejects.toThrow('Invalid buyer: invalid-address') + }) + }) + + describe('claim', () => { + it('should successfully claim a reserved NFD', async () => { + const result = await purchasing.claim('reserved.algo') + + expect(mockAlgorand.createTransaction.payment).toHaveBeenCalledWith({ + sender: BUYER_ADDRESS, + receiver: 'mock-app-address', + amount: expect.objectContaining({ + amountInMicroAlgo: BigInt(400000), // sellAmount (500000) - mintingKickoffAmount (100000) + }), + }) + + expect(mockInstanceClient.newGroup).toHaveBeenCalled() + expect(mockInstanceClient.newGroup().purchase).toHaveBeenCalledWith({ + args: { payment: { id: 'mock-txn' } }, + staticFee: expect.objectContaining({ + amountInMicroAlgo: BigInt(4000), + }), + }) + expect( + mockInstanceClient.newGroup().purchase().send, + ).toHaveBeenCalledWith({ + populateAppCallResources: true, + }) + + expect(result).toEqual(mockReservedNfd) + }) + + it('should throw error if claimer is not authorized', async () => { + // Set up a different signer for this test + const otherSigner: MockSigner = { + addr: { toString: () => OTHER_ADDRESS }, + signer: vi.fn(), + } + client.setSigner(OTHER_ADDRESS, otherSigner.signer) + const purchasing2 = new PurchasingModule(client) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(purchasing2 as any, 'getInstanceClient').mockReturnValue( + mockInstanceClient, + ) + + await expect(purchasing2.claim('reserved.algo')).rejects.toThrow( + 'NFD is reserved for ZZAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM, but buyer is CCAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM', + ) + }) + + it('should successfully claim an NFD that is for sale but reserved for claimer', async () => { + const result = await purchasing.claim('forsale-reserved.algo') + + expect(mockAlgorand.createTransaction.payment).toHaveBeenCalledWith({ + sender: BUYER_ADDRESS, + receiver: 'mock-app-address', + amount: expect.objectContaining({ + amountInMicroAlgo: BigInt(1500000), // sellAmount (2000000) - mintingKickoffAmount (500000) + }), + }) + + expect(result).toEqual(mockForSaleReservedNfd) + }) + + it('should throw error for NFD that cannot be claimed', async () => { + await expect(purchasing.claim('forsale.algo')).rejects.toThrow( + 'Cannot claim NFD: forsale.algo (state: forSale)', + ) + }) + }) + + describe('buy', () => { + it('should successfully buy an NFD for sale', async () => { + const result = await purchasing.buy('forsale.algo') + + expect(mockAlgorand.createTransaction.payment).toHaveBeenCalledWith({ + sender: BUYER_ADDRESS, + receiver: 'mock-app-address', + amount: expect.objectContaining({ + amountInMicroAlgo: BigInt(1000000), + }), + }) + + expect(mockInstanceClient.newGroup).toHaveBeenCalled() + expect(mockInstanceClient.newGroup().purchase).toHaveBeenCalledWith({ + args: { payment: { id: 'mock-txn' } }, + staticFee: expect.objectContaining({ + amountInMicroAlgo: BigInt(4000), + }), + }) + expect( + mockInstanceClient.newGroup().purchase().send, + ).toHaveBeenCalledWith({ + populateAppCallResources: true, + }) + + expect(result).toEqual(mockForSaleNfd) + }) + + it('should throw error if buyer is not authorized', async () => { + // Set up a different signer for this test + const otherSigner: MockSigner = { + addr: { toString: () => OTHER_ADDRESS }, + signer: vi.fn(), + } + client.setSigner(OTHER_ADDRESS, otherSigner.signer) + const purchasing2 = new PurchasingModule(client) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(purchasing2 as any, 'getInstanceClient').mockReturnValue( + mockInstanceClient, + ) + + await expect(purchasing2.buy('forsale-reserved.algo')).rejects.toThrow( + 'NFD is reserved for ZZAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM, but buyer is CCAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM', + ) + }) + + it('should throw error for NFD that cannot be bought', async () => { + await expect(purchasing.buy('owned.algo')).rejects.toThrow( + 'NFD is not available for purchase (state: owned)', + ) + }) + }) + + describe('canClaim', () => { + it('should return true for claimable NFD', async () => { + const result = await purchasing.canClaim('reserved.algo', BUYER_ADDRESS) + expect(result).toBe(true) + }) + + it('should return false for non-claimable NFD', async () => { + const result = await purchasing.canClaim('forsale.algo', BUYER_ADDRESS) + expect(result).toBe(false) + }) + + it('should return false on error', async () => { + const result = await purchasing.canClaim( + 'nonexistent.algo', + BUYER_ADDRESS, + ) + expect(result).toBe(false) + }) + }) + + describe('canBuy', () => { + it('should return true for buyable NFD', async () => { + const result = await purchasing.canBuy('forsale.algo', BUYER_ADDRESS) + expect(result).toBe(true) + }) + + it('should return false for non-buyable NFD', async () => { + const result = await purchasing.canBuy('owned.algo', BUYER_ADDRESS) + expect(result).toBe(false) + }) + + it('should return false on error', async () => { + const result = await purchasing.canBuy('nonexistent.algo', BUYER_ADDRESS) + expect(result).toBe(false) + }) + }) + + describe('address validation', () => { + it('should throw error for invalid buyer address in getPurchaseQuote', async () => { + await expect( + purchasing.getPurchaseQuote('reserved.algo', 'invalid'), + ).rejects.toThrow('Invalid buyer: invalid') + }) + + it('should throw error for invalid buyer address in buy', async () => { + // Mock the signer to return an invalid address when toString() is called + const mockInvalidSigner: MockSigner = { + addr: { toString: () => 'invalid' }, + signer: vi.fn(), + } + + // Create a new client with a valid signer first + const clientWithValidSigner = new NfdClient() + clientWithValidSigner.setSigner(BUYER_ADDRESS, mockSigner.signer) + + // Now override the signer property to use our invalid signer + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(clientWithValidSigner as any)._signer = mockInvalidSigner + + const purchasing2 = new PurchasingModule(clientWithValidSigner) + + // Ensure proper mocking for the new instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(purchasing2 as any, 'getInstanceClient').mockReturnValue( + mockInstanceClient, + ) + + await expect(purchasing2.buy('forsale.algo')).rejects.toThrow( + 'Invalid buyer: invalid', + ) + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2523e98..a71577d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,46 @@ importers: specifier: ^0.23.0 version: 0.23.0(rollup@4.34.8)(vite@4.5.9(@types/node@22.13.9)) + examples/claim-nfd: + dependencies: + '@algorandfoundation/algokit-utils': + specifier: ^8.2.2 + version: 8.2.2(algosdk@3.2.0) + '@txnlab/nfd-sdk': + specifier: workspace:* + version: link:../../packages/sdk + '@txnlab/use-wallet-react': + specifier: ^4.0.0 + version: 4.0.0(algosdk@3.2.0)(lute-connect@1.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + algosdk: + specifier: ^3.2.0 + version: 3.2.0 + lute-connect: + specifier: ^1.4.1 + version: 1.4.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.2.64 + version: 18.3.18 + '@types/react-dom': + specifier: ^18.2.21 + version: 18.3.5(@types/react@18.3.18) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.3.4(vite@4.5.9(@types/node@22.13.9)) + vite: + specifier: ^4.5.2 + version: 4.5.9(@types/node@22.13.9) + vite-plugin-node-polyfills: + specifier: ^0.23.0 + version: 0.23.0(rollup@4.34.8)(vite@4.5.9(@types/node@22.13.9)) + examples/link-address: dependencies: '@algorandfoundation/algokit-utils':