diff --git a/examples/link-address/src/App.tsx b/examples/link-address/src/App.tsx index 02cb6cf..6e81187 100644 --- a/examples/link-address/src/App.tsx +++ b/examples/link-address/src/App.tsx @@ -17,17 +17,22 @@ export function App() { const [isLoading, setIsLoading] = useState(false) const [isLinking, setIsLinking] = useState(false) const [isUnlinking, setIsUnlinking] = useState(false) + const [isSettingPrimary, setIsSettingPrimary] = useState(false) const [linkedAddresses, setLinkedAddresses] = useState([]) - const { activeAddress, transactionSigner } = useWallet() + const { activeAddress, activeWalletAddresses, transactionSigner } = + useWallet() const handleNfdInputChange = (e: React.ChangeEvent) => { setNfdData(null) setNfdName(e.target.value) setLinkedAddresses([]) + setAddressToLink('') } - const handleAddressInputChange = (e: React.ChangeEvent) => { + const handleAddressSelectChange = ( + e: React.ChangeEvent, + ) => { setAddressToLink(e.target.value) } @@ -36,6 +41,7 @@ export function App() { setError('') setNfdData(null) setLinkedAddresses([]) + setAddressToLink('') setIsLoading(true) try { @@ -81,8 +87,8 @@ export function App() { throw new Error('No NFD selected') } - if (!addressToLink.trim()) { - throw new Error('Please enter an address to link') + if (!addressToLink) { + throw new Error('Please select an address to link') } // Check if the address is already linked @@ -95,6 +101,16 @@ export function App() { throw new Error('Only the owner can link addresses to this NFD') } + // Check if the address to link is in the available addresses + if ( + !activeWalletAddresses || + !activeWalletAddresses.includes(addressToLink) + ) { + throw new Error( + 'The address to link must be one of your connected wallet addresses', + ) + } + /** * Link the address to the NFD */ @@ -170,6 +186,58 @@ export function App() { } } + const handleSetPrimaryAddress = async (address: string) => { + setError('') + setIsSettingPrimary(true) + + try { + if (!activeAddress) { + throw new Error('No active address') + } + + if (!nfdData) { + throw new Error('No NFD selected') + } + + // Check if the active address is the owner + if (nfdData.owner !== activeAddress) { + throw new Error( + 'Only the owner can set the primary address for this NFD', + ) + } + + /** + * Set the address as primary for the NFD + */ + const updatedNfd = await nfd + .setSigner(activeAddress, transactionSigner) + .manage(nfdName) + .setPrimaryAddress(address) + + setNfdData(updatedNfd) + + // Update the linked addresses list + const addresses: string[] = [] + if (updatedNfd.caAlgo && updatedNfd.caAlgo.length > 0) { + updatedNfd.caAlgo.forEach((addr) => { + if (addr) { + addresses.push(addr) + } + }) + } + setLinkedAddresses(addresses) + } catch (err) { + setError(parseTransactionError(err)) + console.error(err) + } finally { + setIsSettingPrimary(false) + } + } + + const availableAddresses = activeWalletAddresses + ? activeWalletAddresses.filter((addr) => !linkedAddresses.includes(addr)) + : [] + return (

NFD Address Linker

@@ -245,28 +313,53 @@ export function App() {

No addresses linked to this NFD.

) : (
    - {linkedAddresses.map((address) => ( -
  • + {linkedAddresses.map((address, index) => ( +
  • {address} - {nfdData.owner === activeAddress && ( - + Primary + + )} + {nfdData.owner === activeAddress && ( +
    + {index > 0 && ( + + )} + +
    )}
  • @@ -278,20 +371,44 @@ export function App() { {nfdData.owner === activeAddress && (

    Link New Address

    -
    - - +
    + {availableAddresses.length > 0 ? ( + + ) : ( +

    + No available addresses to link. +

    + )} +
    + +
    )} diff --git a/examples/link-address/src/Connect.tsx b/examples/link-address/src/Connect.tsx index c6e9d7f..cf13821 100644 --- a/examples/link-address/src/Connect.tsx +++ b/examples/link-address/src/Connect.tsx @@ -79,10 +79,11 @@ const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { @@ -90,7 +91,7 @@ const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { {wallet.activeAccount && (
    - {wallet.activeAccount.address} + Active Account: {wallet.activeAccount.address}
    )} diff --git a/examples/mint/src/Connect.tsx b/examples/mint/src/Connect.tsx index c6e9d7f..cf13821 100644 --- a/examples/mint/src/Connect.tsx +++ b/examples/mint/src/Connect.tsx @@ -79,10 +79,11 @@ const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { @@ -90,7 +91,7 @@ const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { {wallet.activeAccount && (
    - {wallet.activeAccount.address} + Active Account: {wallet.activeAccount.address}
    )} diff --git a/examples/set-metadata/src/Connect.tsx b/examples/set-metadata/src/Connect.tsx index c6e9d7f..cf13821 100644 --- a/examples/set-metadata/src/Connect.tsx +++ b/examples/set-metadata/src/Connect.tsx @@ -79,10 +79,11 @@ const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { @@ -90,7 +91,7 @@ const ConnectedWallet = ({ wallet }: { wallet: Wallet }) => { {wallet.activeAccount && (
    - {wallet.activeAccount.address} + Active Account: {wallet.activeAccount.address}
    )} diff --git a/packages/sdk/src/modules/base.ts b/packages/sdk/src/modules/base.ts index f0507f7..1f6ef14 100644 --- a/packages/sdk/src/modules/base.ts +++ b/packages/sdk/src/modules/base.ts @@ -41,12 +41,15 @@ export abstract class BaseModule { /** * Ensure a signer is set before proceeding + * @returns The current signer * @throws If no signer is set */ - protected requireSigner(): void { - if (!this.getSigner()) { + protected requireSigner(): TransactionSignerAccount { + const signer = this.getSigner() + if (!signer) { throw new Error('Signer required. Call setSigner() first.') } + return signer } /** diff --git a/packages/sdk/src/modules/lookup.ts b/packages/sdk/src/modules/lookup.ts index 781e084..889d8b3 100644 --- a/packages/sdk/src/modules/lookup.ts +++ b/packages/sdk/src/modules/lookup.ts @@ -1,5 +1,6 @@ import { Address } from 'algosdk' +import { isZeroBytes } from '../utils/internal/bytes' import { determineNfdState, generateMetaTags } from '../utils/internal/nfd' import { parseAddress, parseString, parseUint64 } from '../utils/internal/state' import { isValidName } from '../utils/nfd' @@ -179,6 +180,12 @@ export class LookupModule extends BaseModule { // Split the value into chunks of 32 bytes for (let i = 0; i < value.length; i += PUBLIC_KEY_LENGTH) { const publicKey = value.slice(i, i + PUBLIC_KEY_LENGTH) + + // Skip zero addresses (all bytes are zero) + if (isZeroBytes(publicKey)) { + continue + } + try { const address = new Address(publicKey).toString() if (address) { @@ -193,6 +200,11 @@ export class LookupModule extends BaseModule { ) } } + + // Set the verified.caAlgo property as a comma-delimited string + if (caAlgo.length > 0) { + verified.caAlgo = caAlgo.join(',') + } } catch (error) { console.error('Failed to parse Algorand addresses from box:', error) } diff --git a/packages/sdk/src/modules/manager.ts b/packages/sdk/src/modules/manager.ts index 2bd1ebe..0914a28 100644 --- a/packages/sdk/src/modules/manager.ts +++ b/packages/sdk/src/modules/manager.ts @@ -1,4 +1,5 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' +import { Address } from 'algosdk' import { parseTransactionError } from '../utils/error-parser' import { strToUint8Array, concatUint8Arrays } from '../utils/internal/bytes' @@ -74,33 +75,38 @@ export class NfdManager extends BaseModule { * @returns The updated NFD * @throws If the address cannot be linked */ - public async linkAddress(address: string): Promise { - this.requireSigner() + public async linkAddress(address: string | Address): Promise { + const signer = this.requireSigner() const nfd = await this.getNfd() - // Ensure the caller is the owner - const signer = this.getSigner() - if (!signer || signer.addr.toString() !== nfd.owner) { + // Ensure the default signer is the owner + if (signer.addr.toString() !== nfd.owner) { throw new Error('Only the owner can link addresses to this NFD') } + // Get the Address to link + const addressToLink = + typeof address === 'string' ? Address.fromString(address) : address + + // If address to link is not the default signer (owner), add a signer for it + if (signer.addr !== addressToLink) { + this.algorand.setSigner(addressToLink, signer.signer) + } + // Get the NFD instance client if (!nfd.appID) { throw new Error('NFD has no application ID') } const nfdAppId = BigInt(nfd.appID) - const nfdInstanceClient = this.getInstanceClient( - nfdAppId, - signer.addr.toString(), - ) + const nfdInstanceClient = this.getInstanceClient(nfdAppId, signer.addr) // Prepare the fields to update const fieldsToUpdate: Uint8Array[] = [ strToUint8Array('u.cav.algo.a'), - signer.addr.publicKey, + addressToLink.publicKey, ] - // Get current box value/size in NFD + // Get current v.caAlgo.0.as box value/size let curCaAlgo: Uint8Array try { curCaAlgo = await nfdInstanceClient.appClient.getBoxValue('v.caAlgo.0.as') @@ -113,7 +119,7 @@ export class NfdManager extends BaseModule { fieldsToUpdate[0], fieldsToUpdate[1], strToUint8Array('v.caAlgo.0.as'), - concatUint8Arrays(curCaAlgo, signer.addr.publicKey), + concatUint8Arrays(curCaAlgo, addressToLink.publicKey), ] // Get the cost of the update @@ -129,7 +135,7 @@ export class NfdManager extends BaseModule { // Add payment transaction for the update cost const updatePaymentTxn = await this.algorand.createTransaction.payment({ - sender: signer.addr.toString(), + sender: signer.addr, receiver: nfdInstanceClient.appAddress, amount: AlgoAmount.MicroAlgos(updateCost), }) @@ -138,12 +144,12 @@ export class NfdManager extends BaseModule { nfdNewGroup.updateFields({ args: { fieldAndVals: fieldsToUpdate } }) // Get the registry client - const registryClient = this.getRegistryClient(signer.addr.toString()) + const registryClient = this.getRegistryClient(signer.addr) // Get the cost to add to registry const regMbrCostResult = await registryClient .newGroup() - .costToAddToAddress({ args: { lookupAddress: address } }) + .costToAddToAddress({ args: { lookupAddress: addressToLink.toString() } }) .simulate({ skipSignatures: true, allowUnnamedResources: true }) const regMbrCost = regMbrCostResult.returns[0] || 0n @@ -151,7 +157,7 @@ export class NfdManager extends BaseModule { // Add payment transaction if needed if (regMbrCost > 0n) { const regPaymentTxn = await this.algorand.createTransaction.payment({ - sender: signer.addr.toString(), + sender: signer.addr, receiver: registryClient.appAddress, amount: AlgoAmount.MicroAlgos(regMbrCost), }) @@ -165,9 +171,9 @@ export class NfdManager extends BaseModule { args: { nfdName: nfd.name, nfdAppId: nfdInstanceClient.appId, - addrToVerify: address, + addrToVerify: addressToLink.toString(), }, - sender: signer.addr, + sender: addressToLink, staticFee: AlgoAmount.MicroAlgos(3000), }) @@ -191,28 +197,28 @@ export class NfdManager extends BaseModule { * @returns The updated NFD * @throws If the address cannot be unlinked */ - public async unlinkAddress(address: string): Promise { - this.requireSigner() + public async unlinkAddress(address: string | Address): Promise { + const signer = this.requireSigner() const nfd = await this.getNfd() - // Ensure the caller is the owner - const signer = this.getSigner() - if (!signer || signer.addr.toString() !== nfd.owner) { + // Ensure the default signer is the owner + if (signer.addr.toString() !== nfd.owner) { throw new Error('Only the owner can unlink addresses from this NFD') } + // Get the Address to unlink + const addressToUnlink = + typeof address === 'string' ? Address.fromString(address) : address + // Get the NFD instance client if (!nfd.appID) { throw new Error('NFD has no application ID') } const nfdAppId = BigInt(nfd.appID) - const nfdInstanceClient = this.getInstanceClient( - nfdAppId, - signer.addr.toString(), - ) + const nfdInstanceClient = this.getInstanceClient(nfdAppId, signer.addr) // Get the registry client - const registryClient = this.getRegistryClient(signer.addr.toString()) + const registryClient = this.getRegistryClient(signer.addr) try { // Create and execute the unlink transaction @@ -222,7 +228,7 @@ export class NfdManager extends BaseModule { args: { nfdName: nfd.name, nfdAppId: nfdInstanceClient.appId, - addrToUnlink: address, + addrToUnlink: addressToUnlink.toString(), }, staticFee: AlgoAmount.MicroAlgos(5000), }) @@ -245,12 +251,11 @@ export class NfdManager extends BaseModule { * @throws If the metadata cannot be set */ public async setMetadata(metadata: Record): Promise { - this.requireSigner() + const signer = this.requireSigner() const nfd = await this.getNfd() - // Ensure the caller is the owner - const signer = this.getSigner() - if (!signer || signer.addr.toString() !== nfd.owner) { + // Ensure the default signer is the owner + if (signer.addr.toString() !== nfd.owner) { throw new Error('Only the owner can set metadata for this NFD') } @@ -259,10 +264,7 @@ export class NfdManager extends BaseModule { throw new Error('NFD has no application ID') } const nfdAppId = BigInt(nfd.appID) - const nfdInstanceClient = this.getInstanceClient( - nfdAppId, - signer.addr.toString(), - ) + const nfdInstanceClient = this.getInstanceClient(nfdAppId, signer.addr) // Convert metadata object to array of fields and values const fieldsAndValues: string[] = [] @@ -340,4 +342,56 @@ export class NfdManager extends BaseModule { this._nfd = null return this.getNfd() } + + /** + * Set a specific address as the primary address for the NFD + * @param address - The Algorand address to set as primary + * @returns The updated NFD + * @throws If the address cannot be set as primary + */ + public async setPrimaryAddress(address: string | Address): Promise { + const signer = this.requireSigner() + const nfd = await this.getNfd() + + // Ensure the default signer is the owner + if (signer.addr.toString() !== nfd.owner) { + throw new Error('Only the owner can set the primary address for this NFD') + } + + // Get the Address to set as primary + const addressToSet = + typeof address === 'string' ? Address.fromString(address) : address + + // Get the NFD instance client + if (!nfd.appID) { + throw new Error('NFD has no application ID') + } + const nfdAppId = BigInt(nfd.appID) + const nfdInstanceClient = this.getInstanceClient(nfdAppId, signer.addr) + + // The only supported field for now is 'v.caAlgo.0.as' + const fieldName = 'v.caAlgo.0.as' + + try { + // Create and execute the setPrimaryAddress transaction + await nfdInstanceClient + .newGroup() + .setPrimaryAddress({ + args: { + fieldName, + address: addressToSet.toString(), + }, + staticFee: AlgoAmount.MicroAlgos(3000), + }) + .send({ populateAppCallResources: true }) + } catch (error) { + throw new Error( + `Failed to set primary address: ${parseTransactionError(error)}`, + ) + } + + // Refresh the NFD data + this._nfd = null + return this.getNfd() + } } diff --git a/packages/sdk/src/utils/internal/bytes.ts b/packages/sdk/src/utils/internal/bytes.ts index d4ba228..d202dc5 100644 --- a/packages/sdk/src/utils/internal/bytes.ts +++ b/packages/sdk/src/utils/internal/bytes.ts @@ -27,3 +27,12 @@ export function concatUint8Arrays( return concatenatedArray } + +/** + * Check if a Uint8Array contains only zero bytes + * @param array - The array to check + * @returns True if all bytes are zero, false otherwise + */ +export function isZeroBytes(array: Uint8Array): boolean { + return array.every((byte) => byte === 0) +}