From fab40536441fe069a556266769d0f35a57bea777 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Mon, 10 Nov 2025 19:03:53 +0800 Subject: [PATCH 01/30] feat: initial spark tokenization implementation --- src/sdk/sponsorship/index.ts | 21 +- .../wallet-developer-controlled/cardano.ts | 155 +++++ src/sdk/wallet-developer-controlled/index.ts | 242 +++---- src/sdk/wallet-developer-controlled/spark.ts | 196 ++++++ src/spark/web3-spark-wallet.ts | 591 ++++++++++++++++++ src/types/core/index.ts | 13 +- src/types/spark/index.ts | 82 +++ src/types/window/open-window-params.ts | 80 +++ src/types/window/open-window-result.ts | 56 ++ 9 files changed, 1277 insertions(+), 159 deletions(-) create mode 100644 src/sdk/wallet-developer-controlled/cardano.ts create mode 100644 src/sdk/wallet-developer-controlled/spark.ts create mode 100644 src/spark/web3-spark-wallet.ts diff --git a/src/sdk/sponsorship/index.ts b/src/sdk/sponsorship/index.ts index 537d359..5e3cd7d 100644 --- a/src/sdk/sponsorship/index.ts +++ b/src/sdk/sponsorship/index.ts @@ -1,5 +1,5 @@ import { Web3Sdk } from ".."; -import { UTxO, MeshTxBuilder } from "@meshsdk/core"; +import { UTxO, MeshTxBuilder, Asset } from "@meshsdk/core"; import { meshUniversalStaticUtxo } from "../index"; import { SponsorshipTxParserPostRequestBody } from "../../types"; @@ -213,11 +213,13 @@ export class Sponsorship { if (!isUtxoUsed) { selectedUtxo = _selectedUtxo; - await this.dbAppendUtxosUsed( - config, - selectedUtxo.input.txHash, - selectedUtxo.input.outputIndex, - ); + if (selectedUtxo) { + await this.dbAppendUtxosUsed( + config, + selectedUtxo.input.txHash, + selectedUtxo.input.outputIndex, + ); + } } } @@ -339,7 +341,8 @@ export class Sponsorship { private async getSponsorWallet(projectWalletId: string) { const networkId = this.sdk.network === "mainnet" ? 1 : 0; - const wallet = await this.sdk.wallet.getWallet(projectWalletId, networkId); + // For sponsorship, we assume Cardano wallets (since this is for Cardano sponsorship) + const wallet = await this.sdk.wallet.getWallet("cardano", projectWalletId, { networkId }); return wallet.wallet; } @@ -442,7 +445,7 @@ export class Sponsorship { // Add balance from UTXOs that are not the exact sponsor amount for (const utxo of utxosAsInput) { const lovelaceAmount = utxo.output.amount.find( - (amount) => amount.unit === "lovelace", + (amount: Asset) => amount.unit === "lovelace", ); if (lovelaceAmount) { totalBalance += parseInt(lovelaceAmount.quantity); @@ -452,7 +455,7 @@ export class Sponsorship { // Add balance from UTXOs that were pending for too long for (const utxo of utxosNotSpentAfterDuration) { const lovelaceAmount = utxo.output.amount.find( - (amount) => amount.unit === "lovelace", + (amount: Asset) => amount.unit === "lovelace", ); if (lovelaceAmount) { totalBalance += parseInt(lovelaceAmount.quantity); diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts new file mode 100644 index 0000000..753ad75 --- /dev/null +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -0,0 +1,155 @@ +import { Web3Sdk } from ".."; +import { MeshWallet } from "@meshsdk/wallet"; +import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; +import { Web3ProjectCardanoWallet } from "../../types"; +import { deserializeBech32Address } from "@meshsdk/core-cst"; +import { v4 as uuidv4 } from "uuid"; + +/** + * CardanoWalletDeveloperControlled - Manages Cardano-specific developer-controlled wallets + */ +export class CardanoWalletDeveloperControlled { + readonly sdk: Web3Sdk; + + constructor({ sdk }: { sdk: Web3Sdk }) { + this.sdk = sdk; + } + + /** + * Creates a new Cardano wallet associated with the current project. + */ + async createWallet({ + tags, + }: { tags?: string[] } = {}): Promise { + const project = await this.sdk.getProject(); + + if (!project.publicKey) { + throw new Error("Project public key not found"); + } + + const mnemonic = MeshWallet.brew() as string[]; + const encryptedMnemonic = await encryptWithPublicKey({ + publicKey: project.publicKey, + data: mnemonic.join(" "), + }); + + const _wallet = new MeshWallet({ + networkId: 1, + key: { + type: "mnemonic", + words: mnemonic, + }, + fetcher: this.sdk.providerFetcher, + submitter: this.sdk.providerSubmitter, + }); + await _wallet.init(); + + const addresses = await _wallet.getAddresses(); + const baseAddressBech32 = addresses.baseAddressBech32!; + + const { pubKeyHash, stakeCredentialHash } = + deserializeBech32Address(baseAddressBech32); + + const web3Wallet: Web3ProjectCardanoWallet = { + id: uuidv4(), + key: encryptedMnemonic, + tags: tags || [], + projectId: this.sdk.projectId, + pubKeyHash: pubKeyHash, + stakeCredentialHash: stakeCredentialHash, + }; + + const { data, status } = await this.sdk.axiosInstance.post( + `api/project-wallet/cardano`, + web3Wallet, + ); + + if (status === 200) { + return data as Web3ProjectCardanoWallet; + } + + throw new Error("Failed to create Cardano wallet"); + } + + /** + * Retrieves all Cardano wallets for the project + */ + async getWallets(): Promise { + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/cardano`, + ); + + if (status === 200) { + return data as Web3ProjectCardanoWallet[]; + } + + throw new Error("Failed to get Cardano wallets"); + } + + /** + * Retrieves a specific Cardano wallet by ID + */ + async getWallet( + walletId: string, + networkId: 0 | 1, + decryptKey = false, + ): Promise<{ + info: Web3ProjectCardanoWallet; + wallet: MeshWallet; + }> { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found"); + } + + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/cardano/${walletId}`, + ); + + if (status === 200) { + const web3Wallet = data as Web3ProjectCardanoWallet; + + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: web3Wallet.key, + }); + + if (decryptKey) { + web3Wallet.key = mnemonic; + } + + const wallet = new MeshWallet({ + networkId: networkId, + key: { + type: "mnemonic", + words: mnemonic.split(" "), + }, + fetcher: this.sdk.providerFetcher, + submitter: this.sdk.providerSubmitter, + }); + await wallet.init(); + + return { info: web3Wallet, wallet: wallet }; + } + + throw new Error("Failed to get Cardano wallet"); + } + + /** + * Get Cardano wallets by tag + */ + async getWalletsByTag(tag: string): Promise { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found"); + } + + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/cardano/tag/${tag}`, + ); + + if (status === 200) { + return data as Web3ProjectCardanoWallet[]; + } + + throw new Error("Failed to get Cardano wallets by tag"); + } +} \ No newline at end of file diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index d70e2bc..3bda74d 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -1,180 +1,124 @@ import { Web3Sdk } from ".."; -import { MeshWallet } from "@meshsdk/wallet"; -import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; -import { Web3ProjectWallet } from "../../types"; -import { deserializeBech32Address } from "@meshsdk/core-cst"; -import { v4 as uuidv4 } from "uuid"; +import { Web3ProjectCardanoWallet, Web3ProjectSparkWallet } from "../../types"; +import { CardanoWalletDeveloperControlled } from "./cardano"; +import { SparkWalletDeveloperControlled } from "./spark"; + +// Export chain-specific classes +export { CardanoWalletDeveloperControlled } from "./cardano"; +export { SparkWalletDeveloperControlled } from "./spark"; /** * The `WalletDeveloperControlled` class provides functionality for managing developer-controlled wallets - * within a Web3 project. It allows for creating wallets, retrieving wallet information, and accessing - * specific wallets using their identifiers. + * within a Web3 project. It orchestrates chain-specific wallet management for Cardano and Spark. */ export class WalletDeveloperControlled { readonly sdk: Web3Sdk; + readonly cardano: CardanoWalletDeveloperControlled; + readonly spark: SparkWalletDeveloperControlled; constructor({ sdk }: { sdk: Web3Sdk }) { - { - this.sdk = sdk; - } + this.sdk = sdk; + this.cardano = new CardanoWalletDeveloperControlled({ sdk }); + this.spark = new SparkWalletDeveloperControlled({ sdk }); } /** - * Creates a new wallet associated with the current project. - * This method generates a new wallet encrypts it with the project's public key, and registers the wallet with the backend service. - * - * @param {Object} [options] - Optional parameters for wallet creation. - * @param {string} [options.tag] - An optional tag to associate with the wallet. - * - * @returns {Promise} A promise that resolves to the created wallet instance. - * - * @throws {Error} If the project's public key is not found. - * @throws {Error} If the wallet creation request to the backend fails. + * Creates a new wallet for the specified chain */ - async createWallet({ - tags, - }: { tags?: string[] } = {}): Promise { - const project = await this.sdk.getProject(); - - if (!project.publicKey) { - throw new Error("Project public key not found"); - } - - const mnemonic = MeshWallet.brew() as string[]; - const encryptedMnemonic = await encryptWithPublicKey({ - publicKey: project.publicKey, - data: mnemonic.join(" "), - }); - - const _wallet = new MeshWallet({ - networkId: 1, - key: { - type: "mnemonic", - words: mnemonic, - }, - fetcher: this.sdk.providerFetcher, - submitter: this.sdk.providerSubmitter, - }); - await _wallet.init(); - - const addresses = await _wallet.getAddresses(); - const baseAddressBech32 = addresses.baseAddressBech32!; - - const { pubKeyHash, stakeCredentialHash } = - deserializeBech32Address(baseAddressBech32); - - // create wallet - - const web3Wallet: Web3ProjectWallet = { - id: uuidv4(), - key: encryptedMnemonic, - tags: tags || [], - projectId: this.sdk.projectId, - pubKeyHash: pubKeyHash, - stakeCredentialHash: stakeCredentialHash, - }; - - const { data, status } = await this.sdk.axiosInstance.post( - `api/project-wallet`, - web3Wallet, - ); - - if (status === 200) { - return data as Web3ProjectWallet; + async createWallet( + chain: "cardano" | "spark", + options: { + tags?: string[]; + network?: "MAINNET" | "REGTEST"; // For Spark only + purpose?: "tokenization" | "general"; // For Spark only + } = {} + ): Promise { + if (chain === "cardano") { + return this.cardano.createWallet({ tags: options.tags }); + } else if (chain === "spark") { + return this.spark.createWallet({ + tags: options.tags, + network: options.network, + purpose: options.purpose, + }); } - - throw new Error("Failed to create wallet"); + + throw new Error(`Unsupported chain: ${chain}`); } /** - * Retrieves a list of wallets associated with the current project. - * - * @returns {Promise} A promise that resolves to an array of wallets, - * each containing the wallet's `id`, `address`, `networkId`, and `tag`. - * - * @throws {Error} Throws an error if the request to fetch wallets fails. + * Get wallets for a specific chain */ - async getWallets(): Promise { - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}`, - ); - - if (status === 200) { - return data as Web3ProjectWallet[]; + async getWallets( + chain: "cardano" | "spark" + ): Promise { + if (chain === "cardano") { + return this.cardano.getWallets(); + } else if (chain === "spark") { + return this.spark.getWallets(); } - - throw new Error("Failed to get wallets"); + + throw new Error(`Unsupported chain: ${chain}`); } /** - * Retrieves a wallet by its ID and decrypts the key with the project's private key. - * - * @param walletId - The unique identifier of the wallet to retrieve. - * @param networkId - The network ID associated with the wallet (0 or 1). - * @param decryptKey - A boolean indicating whether to decrypt the wallet key (default: false). - * - * @returns A promise that resolves to an initialized `MeshWallet` instance. - * @throws Will throw an error if the private key is not found or if the wallet retrieval fails. + * Get a specific wallet by ID and chain */ async getWallet( + chain: "cardano" | "spark", walletId: string, - networkId: 0 | 1, - decryptKey = false, - ): Promise<{ - info: Web3ProjectWallet; - wallet: MeshWallet; - }> { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found"); - } - - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/${walletId}`, - ); - - if (status === 200) { - const web3Wallet = data as Web3ProjectWallet; - - const mnemonic = await decryptWithPrivateKey({ - privateKey: this.sdk.privateKey, - encryptedDataJSON: web3Wallet.key, - }); - - if (decryptKey) { - web3Wallet.key = mnemonic; - } - - const wallet = new MeshWallet({ - networkId: networkId, - key: { - type: "mnemonic", - words: mnemonic.split(" "), - }, - fetcher: this.sdk.providerFetcher, - submitter: this.sdk.providerSubmitter, - }); - await wallet.init(); - - return { info: web3Wallet, wallet: wallet }; + options: { + networkId?: 0 | 1; // For Cardano only + decryptKey?: boolean; + } = {} + ): Promise { + if (chain === "cardano") { + return this.cardano.getWallet( + walletId, + options.networkId ?? 1, + options.decryptKey + ); + } else if (chain === "spark") { + return this.spark.getWallet(walletId, options.decryptKey); } - - throw new Error("Failed to get wallet"); + + throw new Error(`Unsupported chain: ${chain}`); } - async getWalletsByTag(tag: string): Promise { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found"); - } - - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/tag/${tag}`, - ); - - if (status === 200) { - const web3Wallets = data as Web3ProjectWallet[]; - return web3Wallets; - } + /** + * Get all wallets (both Cardano and Spark) for the project + */ + async getAllWallets(): Promise<{ + cardano: Web3ProjectCardanoWallet[]; + spark: Web3ProjectSparkWallet[]; + }> { + const [cardanoWallets, sparkWallets] = await Promise.all([ + this.cardano.getWallets(), + this.spark.getWallets(), + ]); + + return { + cardano: cardanoWallets, + spark: sparkWallets, + }; + } - throw new Error("Failed to get wallet"); + /** + * Get wallets by tag across all chains + */ + async getWalletsByTag(tag: string): Promise<{ + cardano: Web3ProjectCardanoWallet[]; + spark: Web3ProjectSparkWallet[]; + }> { + const [cardanoWallets, sparkWallets] = await Promise.all([ + this.cardano.getWalletsByTag(tag), + this.spark.getWalletsByTag(tag), + ]); + + return { + cardano: cardanoWallets, + spark: sparkWallets, + }; } + } diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts new file mode 100644 index 0000000..7218b65 --- /dev/null +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -0,0 +1,196 @@ +import { Web3Sdk } from ".."; +import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; +import { Web3ProjectSparkWallet } from "../../types"; +import { generateMnemonic } from "@meshsdk/common"; +import { v4 as uuidv4 } from "uuid"; + +/** + * SparkWalletDeveloperControlled - Manages Spark-specific developer-controlled wallets + */ +export class SparkWalletDeveloperControlled { + readonly sdk: Web3Sdk; + + constructor({ sdk }: { sdk: Web3Sdk }) { + this.sdk = sdk; + } + + /** + * Creates a new Spark wallet associated with the current project. + */ + async createWallet({ + tags, + network = "REGTEST", + purpose = "general", + }: { + tags?: string[]; + network?: "MAINNET" | "REGTEST"; + purpose?: "tokenization" | "general"; + } = {}): Promise { + const project = await this.sdk.getProject(); + + if (!project.publicKey) { + throw new Error("Project public key not found"); + } + + // Generate mnemonic for Spark wallet + const mnemonic = await generateMnemonic(256); + const encryptedMnemonic = await encryptWithPublicKey({ + publicKey: project.publicKey, + data: mnemonic, + }); + + // Initialize Spark wallet to get address and public key + // Dynamic import to avoid bundling Node.js dependencies + const { SparkWallet } = await import("@buildonspark/spark-sdk"); + const { wallet: sparkWallet } = await SparkWallet.initialize({ + mnemonicOrSeed: mnemonic, + options: { network }, + }); + + const sparkAddress = await sparkWallet.getSparkAddress(); + const publicKey = await sparkWallet.getIdentityPublicKey(); + + const web3Wallet: Web3ProjectSparkWallet = { + id: uuidv4(), + key: encryptedMnemonic, + tags: tags || [], + projectId: this.sdk.projectId, + sparkAddress, + publicKey, + network, + purpose, + }; + + const { data, status } = await this.sdk.axiosInstance.post( + `api/project-wallet/spark`, + web3Wallet, + ); + + if (status === 200) { + return data as Web3ProjectSparkWallet; + } + + throw new Error("Failed to create Spark wallet"); + } + + /** + * Retrieves all Spark wallets for the project + */ + async getWallets(): Promise { + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/spark`, + ); + + if (status === 200) { + return data as Web3ProjectSparkWallet[]; + } + + throw new Error("Failed to get Spark wallets"); + } + + /** + * Retrieves a specific Spark wallet by ID + */ + async getWallet( + walletId: string, + decryptKey = false, + ): Promise<{ + info: Web3ProjectSparkWallet; + wallet: any; // SparkWallet type from @buildonspark/spark-sdk + }> { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found"); + } + + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/spark/${walletId}`, + ); + + if (status === 200) { + const web3Wallet = data as Web3ProjectSparkWallet; + + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: web3Wallet.key, + }); + + if (decryptKey) { + web3Wallet.key = mnemonic; + } + + // Initialize Spark wallet with decrypted mnemonic + const { SparkWallet } = await import("@buildonspark/spark-sdk"); + const { wallet: sparkWallet } = await SparkWallet.initialize({ + mnemonicOrSeed: mnemonic, + options: { network: web3Wallet.network }, + }); + + return { info: web3Wallet, wallet: sparkWallet }; + } + + throw new Error("Failed to get Spark wallet"); + } + + /** + * Get Spark wallets by tag + */ + async getWalletsByTag(tag: string): Promise { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found"); + } + + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/spark/tag/${tag}`, + ); + + if (status === 200) { + return data as Web3ProjectSparkWallet[]; + } + + throw new Error("Failed to get Spark wallets by tag"); + } + + /** + * Get Spark wallets designated as token issuers + */ + async getIssuerWallets(): Promise { + return this.getWalletsByTag("spark-issuer"); + } + + /** + * Designate a Spark wallet as a token issuer + */ + async designateAsIssuer(walletId: string): Promise { + const { status } = await this.sdk.axiosInstance.post( + `api/project-wallet/${this.sdk.projectId}/spark/${walletId}/designate-issuer`, + ); + + if (status !== 200) { + throw new Error("Failed to designate wallet as issuer"); + } + } + + /** + * Remove issuer designation from a Spark wallet + */ + async removeIssuerDesignation(walletId: string): Promise { + const { status } = await this.sdk.axiosInstance.delete( + `api/project-wallet/${this.sdk.projectId}/spark/${walletId}/designate-issuer`, + ); + + if (status !== 200) { + throw new Error("Failed to remove issuer designation"); + } + } + + /** + * Check if project has any designated issuer wallets + */ + async hasIssuerWallet(): Promise<{ hasIssuer: boolean; walletId?: string }> { + const issuerWallets = await this.getIssuerWallets(); + return { + hasIssuer: issuerWallets.length > 0, + walletId: issuerWallets[0]?.id, + }; + } +} \ No newline at end of file diff --git a/src/spark/web3-spark-wallet.ts b/src/spark/web3-spark-wallet.ts new file mode 100644 index 0000000..2b78011 --- /dev/null +++ b/src/spark/web3-spark-wallet.ts @@ -0,0 +1,591 @@ +import axios, { AxiosInstance } from "axios"; +import { ApiError } from "../wallet-user-controlled"; +import { OpenWindowResult, Web3AuthProvider } from "../types"; +import * as Spark from "../types/spark"; +import { openWindow } from "../functions"; +import { getSparkAddressFromPubkey } from "../chains"; + +export type ValidSparkNetwork = "MAINNET" | "REGTEST"; + +export type EnableSparkWalletOptions = { + network: ValidSparkNetwork; + sparkscanApiKey?: string; + projectId?: Web3AuthProvider; + appUrl?: string; + baseUrl?: string; + key?: { + type: "address"; + address: string; + identityPublicKey?: string; + }; +}; + +/** + * Web3SparkWallet - Spark Wallet Implementation for Mesh Web3 SDK + * + * Provides Spark Wallet API-compliant implementation for Bitcoin Layer 2 operations, + * token transfers, and message signing as part of the Mesh Web3 SDK. + * + * @example + * ```typescript + * // Initialize wallet + * const wallet = await Web3SparkWallet.enable({ + * network: "REGTEST", + * projectId: "your-project-id", + * appUrl: "https://your-app.com" + * }); + * + * // Get wallet information + * const address = await wallet.getSparkAddress(); + * const publicKey = await wallet.getIdentityPublicKey(); + * const balance = await wallet.getBalance(); + * + * // Transfer Bitcoin + * const txId = await wallet.transfer({ + * receiverSparkAddress: "spark1q...", + * amountSats: 100000 + * }); + * ``` + */ +export class Web3SparkWallet { + private readonly _axiosInstance: AxiosInstance; + readonly network: ValidSparkNetwork; + private sparkAddress: string = ""; + private publicKey: string = ""; + private projectId?: string; + private appUrl?: string; + + constructor(options: EnableSparkWalletOptions) { + this._axiosInstance = axios.create({ + baseURL: options.baseUrl || "https://api.sparkscan.io", + headers: { + Accept: "application/json", + ...(options.sparkscanApiKey && { + Authorization: `Bearer ${options.sparkscanApiKey}`, + }), + }, + }); + this.network = options.network; + this.projectId = options.projectId; + this.appUrl = options.appUrl; + + if (options.key?.type === "address") { + this.sparkAddress = options.key.address; + this.publicKey = options.key.identityPublicKey || ""; + } + } + + /** + * Enables and initializes a Web3SparkWallet instance + * @param options - Configuration options for the wallet + * @param options.network - Network to connect to ("MAINNET" | "REGTEST") + * @param options.sparkscanApiKey - Optional API key for Sparkscan + * @param options.projectId - Project ID for authentication + * @param options.appUrl - Application URL for iframe communication + * @param options.baseUrl - Optional custom base URL for API calls + * @param options.key - Optional pre-existing wallet key information + * @returns Promise resolving to an initialized Web3SparkWallet instance + * @throws ApiError if wallet initialization fails or user declines + */ + static async enable( + options: EnableSparkWalletOptions, + ): Promise { + if (options.key?.type === "address") { + return new Web3SparkWallet(options); + } + + const networkId = options.network === "MAINNET" ? 1 : 0; + const res: OpenWindowResult = await openWindow( + { + method: "enable", + projectId: options.projectId!, + directTo: options.projectId!, + refreshToken: "undefined", + networkId: String(networkId), + keepWindowOpen: "false", + }, + options.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined to enable Spark wallet.", + }); + + if (res.data.method !== "enable") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + const publicKey = options.network === "MAINNET" + ? res.data.sparkMainnetPubKeyHash + : res.data.sparkRegtestPubKeyHash; + + const sparkAddress = getSparkAddressFromPubkey(publicKey, options.network); + + return new Web3SparkWallet({ + network: options.network, + sparkscanApiKey: options.sparkscanApiKey, + projectId: options.projectId, + appUrl: options.appUrl, + key: { + type: "address", + address: sparkAddress, + identityPublicKey: publicKey, + }, + }); + } + + /** + * Create a new token (basic implementation - will be enhanced by SparkTokenIssuer) + */ + async createToken(params: Spark.TokenCreationParams): Promise { + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Token creation requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-create-token", + projectId: this.projectId, + networkId: String(networkId), + tokenName: params.tokenName, + tokenTicker: params.tokenTicker, + decimals: String(params.decimals), + maxSupply: params.maxSupply.toString(), + isFreezable: params.isFreezable ? "true" : "false", + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined to create token.", + }); + + if (res.data.method !== "spark-create-token") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txId || res.data.tokenId || ''; + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to create token: " + error, + }); + } + } + + /** + * Mint tokens to a specific address (issuer only) + * @param params - Minting parameters + * @returns Promise resolving to the mint transaction ID + */ + async mintTokens(params: Spark.MintTokenParams): Promise; + /** + * Mint tokens to a specific address (issuer only) - legacy signature + * @param tokenIdentifier - The token identifier (btkn1...) + * @param amount - Amount of tokens to mint in base units + * @param recipientAddress - Recipient address + * @returns Promise resolving to the mint transaction ID + */ + async mintTokens(tokenIdentifier: string, amount: bigint, recipientAddress: string): Promise; + async mintTokens( + paramsOrTokenIdentifier: Spark.MintTokenParams | string, + amount?: bigint, + recipientAddress?: string + ): Promise { + // Handle both signatures + const params: Spark.MintTokenParams = typeof paramsOrTokenIdentifier === 'string' + ? { tokenIdentifier: paramsOrTokenIdentifier, amount: amount!, recipientAddress: recipientAddress! } + : paramsOrTokenIdentifier; + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Token minting requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-mint-tokens", + projectId: this.projectId, + networkId: String(networkId), + tokenIdentifier: params.tokenIdentifier, + amount: params.amount.toString(), + recipientAddress: params.recipientAddress, + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined to mint tokens.", + }); + + if (res.data.method !== "spark-mint-tokens") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txId; + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to mint tokens: " + error, + }); + } + } + + /** + * Freeze a Spark address from transferring tokens + * Note: Spark does not support clawback, only freeze/unfreeze + * @param tokenIdentifier - The token identifier (btkn1...) + * @param address - The Spark address to freeze + * @param reason - Optional reason for freezing + * @returns Promise resolving to the freeze transaction ID + */ + async freezeTokens(tokenIdentifier: string, address: string, reason?: string): Promise { + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Token freezing requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-freeze-address", + projectId: this.projectId, + networkId: String(networkId), + tokenIdentifier, + address, + reason, + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined to freeze tokens.", + }); + + if (res.data.method !== "spark-freeze-address") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txId; + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to freeze tokens: " + error, + }); + } + } + + /** + * Unfreeze a Spark address to allow token transfers + * @param tokenIdentifier - The token identifier (btkn1...) + * @param address - The Spark address to unfreeze + * @returns Promise resolving to the unfreeze transaction ID + */ + async unfreezeTokens(tokenIdentifier: string, address: string): Promise { + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Token unfreezing requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-unfreeze-address", + projectId: this.projectId, + networkId: String(networkId), + tokenIdentifier, + address, + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined to unfreeze tokens.", + }); + + if (res.data.method !== "spark-unfreeze-address") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txId; + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to unfreeze tokens: " + error, + }); + } + } + + /** + * Burn tokens permanently from circulation + * @param tokenIdentifier - The token identifier (btkn1...) + * @param amount - Amount of tokens to burn in base units + * @returns Promise resolving to the burn transaction ID + */ + async burnTokens(tokenIdentifier: string, amount: bigint): Promise { + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Token burning requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-burn-tokens", + projectId: this.projectId, + networkId: String(networkId), + tokenIdentifier, + amount: amount.toString(), + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined to burn tokens.", + }); + + if (res.data.method !== "spark-burn-tokens") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txId; + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to burn tokens: " + error, + }); + } + } + + /** + * Transfer Spark tokens to a single recipient + * @param tokenIdentifier - The token identifier (btkn1...) + * @param recipientAddress - Recipient's Spark address + * @param amount - Amount to transfer in base units + * @returns Promise resolving to the transfer transaction ID + */ + async transferTokens(tokenIdentifier: string, recipientAddress: string, amount: bigint): Promise { + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Token transfer requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-transfer-tokens", + projectId: this.projectId, + networkId: String(networkId), + tokenIdentifier, + amount: amount.toString(), + recipientAddress, + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined the token transfer.", + }); + + if (res.data.method !== "spark-transfer-tokens") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txId; + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to transfer tokens: " + error, + }); + } + } + + /** + * Batch transfer Spark tokens to multiple recipients + * @param tokenIdentifier - The token identifier (btkn1...) + * @param transfers - Array of recipient addresses and amounts + * @returns Promise resolving to array of transaction IDs + */ + async batchTransferTokens( + tokenIdentifier: string, + transfers: Array<{ recipientAddress: string; amount: bigint }> + ): Promise { + if (!this.projectId || !this.appUrl) { + throw new ApiError({ + code: 1, + info: "Batch transfer requires projectId and appUrl for authentication", + }); + } + + try { + const networkId = this.network === "MAINNET" ? 1 : 0; + + const res: OpenWindowResult = await openWindow( + { + method: "spark-batch-transfer", + projectId: this.projectId, + networkId: String(networkId), + tokenIdentifier, + transfers: JSON.stringify(transfers.map(t => ({ + recipientAddress: t.recipientAddress, + amount: t.amount.toString(), + }))), + }, + this.appUrl, + ); + + if (res.success === false) + throw new ApiError({ + code: 3, + info: "UserDeclined - User declined the batch transfer.", + }); + + if (res.data.method !== "spark-batch-transfer") { + throw new ApiError({ + code: 2, + info: "Received the wrong response from the iframe.", + }); + } + + return res.data.txIds || (res.data.txId ? [res.data.txId] : []); + } catch (error) { + throw new ApiError({ + code: 4, + info: "Failed to execute batch transfer: " + error, + }); + } + } + + /** + * Get all token balances for an address using Sparkscan API + * Follows the official API: GET /v1/address/{address}/tokens + * @param address - Optional address to query (defaults to wallet address) + * @returns Promise resolving to AddressTokensResponse with all token balances + * @see https://docs.sparkscan.io/api/address#get-address-tokens + */ + async getAddressTokens(address?: string): Promise { + try { + const targetAddress = address || this.sparkAddress; + if (!targetAddress) { + throw new Error("Address is required"); + } + + const params = new URLSearchParams({ + network: this.network, + }); + + const response = await this._axiosInstance.get( + `/v1/address/${targetAddress}/tokens?${params.toString()}` + ); + + return response.data; + } catch (error) { + throw new ApiError({ + code: 5, + info: `Failed to get address tokens: ${error}`, + }); + } + } + + + /** + * Query token transactions using Sparkscan API + * Follows the official API: GET /v1/tokens/{identifier}/transactions + * @param tokenIdentifier - Required token identifier (btkn1...) + * @param limit - Optional limit (default: 25, max: 100) + * @param offset - Optional offset for pagination (default: 0) + * @returns Promise resolving to TokenTransactionsResponse + * @see https://docs.sparkscan.io/api/tokens#get-token-transactions + */ + async queryTokenTransactions( + tokenIdentifier: string, + limit: number = 25, + offset: number = 0 + ): Promise { + try { + if (!tokenIdentifier) { + throw new Error("Token identifier is required"); + } + + const queryLimit = Math.min(Math.max(1, limit), 100); + + const params = new URLSearchParams({ + network: this.network, + limit: queryLimit.toString(), + offset: offset.toString(), + }); + + const response = await this._axiosInstance.get( + `/v1/tokens/${tokenIdentifier}/transactions?${params.toString()}` + ); + + return response.data; + } catch (error) { + throw new ApiError({ + code: 5, + info: `Failed to query token transactions: ${error}`, + }); + } + } +} diff --git a/src/types/core/index.ts b/src/types/core/index.ts index 1e3174b..db793d7 100644 --- a/src/types/core/index.ts +++ b/src/types/core/index.ts @@ -27,7 +27,7 @@ export type Web3ProjectBranding = { appleEnabled?: boolean; }; -export type Web3ProjectWallet = { +export type Web3ProjectCardanoWallet = { id: string; key: string; tags: string[]; @@ -36,6 +36,17 @@ export type Web3ProjectWallet = { stakeCredentialHash: string; }; +export type Web3ProjectSparkWallet = { + id: string; + key: string; + tags: string[]; + projectId: string; + sparkAddress: string; + publicKey: string; + network: "MAINNET" | "REGTEST"; + purpose?: "tokenization" | "general"; +}; + export type Web3JWTBody = { /** User's ID */ sub: string; diff --git a/src/types/spark/index.ts b/src/types/spark/index.ts index c8859a3..be97025 100644 --- a/src/types/spark/index.ts +++ b/src/types/spark/index.ts @@ -46,6 +46,74 @@ export interface TokenMetadata { isFreezable: boolean | null; } +/** + * Token transaction participant info + * Used in TokenTransaction for from/to fields + */ +export interface TokenTransactionParticipant { + type: string; + identifier: string; + pubkey: string; +} + +/** + * Individual token transaction from Sparkscan API + * @see https://docs.sparkscan.io/api/tokens#get-token-transactions + */ +export interface TokenTransaction { + id: string; + type: string; + status: string; + createdAt: string; + updatedAt: string; + from: TokenTransactionParticipant; + to: TokenTransactionParticipant; + amount: number; + valueUsd?: number; + tokenMetadata?: TokenMetadata; + multiIoDetails?: any; +} + +/** + * Token transactions response from Sparkscan API + * @see https://docs.sparkscan.io/api/tokens#get-token-transactions + */ +export interface TokenTransactionsResponse { + meta: { + totalItems: number; + limit: number; + offset: number; + }; + data: TokenTransaction[]; +} + +/** + * Token balance information for an address + */ +export interface AddressTokenBalance { + tokenIdentifier: string; + tokenAddress: string; + name: string; + ticker: string; + decimals: number; + balance: number; + valueUsd?: number; + issuerPublicKey: string; + maxSupply: number | null; + isFreezable: boolean | null; +} + +/** + * Address tokens response from Sparkscan API + * @see https://docs.sparkscan.io/api/address#get-address-tokens + */ +export interface AddressTokensResponse { + address: string; + pubkey: string; + totalValueUsd: number; + tokens: AddressTokenBalance[]; +} + export interface TransactionOutput { address: string; pubkey: string; @@ -165,6 +233,20 @@ export interface LatestTxidResponse { [address: string]: string | null; } +export interface TokenCreationParams { + tokenName: string; + tokenTicker: string; + decimals: number; + maxSupply: number; + isFreezable: boolean; +} + +export interface MintTokenParams { + tokenIdentifier: string; + amount: bigint; + recipientAddress: string; +} + // Types copied from @buildonspark/spark-sdk since they are currently private // Source: https://github.com/buildonspark/spark-sdk export enum TransferDirection { diff --git a/src/types/window/open-window-params.ts b/src/types/window/open-window-params.ts index 959c90e..c880d57 100644 --- a/src/types/window/open-window-params.ts +++ b/src/types/window/open-window-params.ts @@ -78,6 +78,86 @@ export type OpenWindowParams = networkId: string; message: string; } + /** Spark Token Operations */ + | { + method: "spark-create-token"; + projectId: string; + networkId: string; + tokenName: string; + tokenTicker: string; + decimals: string; + maxSupply: string; + isFreezable: "true" | "false"; + } + | { + method: "spark-mint-tokens"; + projectId: string; + networkId: string; + tokenIdentifier: string; + amount: string; + recipientAddress: string; + } + | { + method: "spark-burn-tokens"; + projectId: string; + networkId: string; + tokenIdentifier: string; + amount: string; + } + | { + method: "spark-freeze-address"; + projectId: string; + networkId: string; + tokenIdentifier: string; + address: string; + reason?: string; + } + | { + method: "spark-unfreeze-address"; + projectId: string; + networkId: string; + tokenIdentifier: string; + address: string; + } + | { + method: "spark-transfer-tokens"; + projectId: string; + networkId: string; + tokenIdentifier: string; + amount: string; + recipientAddress: string; + } + | { + method: "spark-batch-transfer"; + projectId: string; + networkId: string; + tokenIdentifier: string; + transfers: string; + } + | { + method: "spark-get-token-balance"; + projectId: string; + networkId: string; + tokenIdentifier: string; + } + | { + method: "spark-get-token-holders"; + projectId: string; + networkId: string; + tokenIdentifier: string; + } + | { + method: "spark-get-token-policy"; + projectId: string; + networkId: string; + tokenIdentifier: string; + } + | { + method: "spark-get-token-analytics"; + projectId: string; + networkId: string; + tokenIdentifier: string; + } /** to be deprecated */ | { method: "sign-tx"; diff --git a/src/types/window/open-window-result.ts b/src/types/window/open-window-result.ts index 97b0e1c..6aeadd1 100644 --- a/src/types/window/open-window-result.ts +++ b/src/types/window/open-window-result.ts @@ -57,6 +57,62 @@ export type OpenWindowResult = method: "spark-sign-message"; signature: string; } + /** Spark Token Operations */ + | { + method: "spark-create-token"; + txId?: string; + tokenId?: string; + } + | { + method: "spark-mint-tokens"; + txId: string; + } + | { + method: "spark-burn-tokens"; + txId: string; + } + | { + method: "spark-freeze-address"; + txId: string; + } + | { + method: "spark-unfreeze-address"; + txId: string; + } + | { + method: "spark-transfer-tokens"; + txId: string; + } + | { + method: "spark-batch-transfer"; + txIds?: string[]; + txId?: string; + } + | { + method: "spark-get-token-balance"; + balance: string; + } + | { + method: "spark-get-token-holders"; + holders: Array<{ + address: string; + balance: string; + }>; + } + | { + method: "spark-get-token-policy"; + policy: any; + } + | { + method: "spark-get-token-analytics"; + analytics: { + totalSupply: string; + circulatingSupply: string; + holdersCount: number; + transactionsCount: number; + frozenAddressesCount: number; + }; + } /** to be deprecated */ | { method: "sign-data"; From 1ddc99a8795a1641370a947a644daaa99db2e908 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 14 Nov 2025 00:04:59 +0800 Subject: [PATCH 02/30] feat: add issuer-sdk dependency and enhance Spark wallet functionality --- package.json | 1 + src/sdk/wallet-developer-controlled/spark.ts | 283 ++++++++++++++++--- src/types/core/index.ts | 1 - 3 files changed, 244 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 924ebf5..b075cc5 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "@buildonspark/issuer-sdk": "^0.0.107", "@buildonspark/spark-sdk": "^0.4.2", "@meshsdk/bitcoin": "1.9.0-beta.84", "@meshsdk/common": "1.9.0-beta.84", diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index 7218b65..cd057de 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -3,9 +3,28 @@ import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; import { Web3ProjectSparkWallet } from "../../types"; import { generateMnemonic } from "@meshsdk/common"; import { v4 as uuidv4 } from "uuid"; +import { IssuerSparkWallet, IssuerTokenMetadata } from "@buildonspark/issuer-sdk"; +import { bitcoin, EmbeddedWallet } from "@meshsdk/bitcoin"; +import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; /** * SparkWalletDeveloperControlled - Manages Spark-specific developer-controlled wallets + * + * This class provides functionality for managing developer-controlled Spark wallets, + * including wallet creation, token operations, and issuer wallet management. + * + * @example + * ```typescript + * const sparkWallet = sdk.wallet.spark; + * const wallet = await sparkWallet.createWallet({ tags: ["my-wallet"] }); + * const tokenResult = await sparkWallet.createToken(wallet.id, { + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * maxSupply: "1000000", + * isFreezable: true + * }); + * ``` */ export class SparkWalletDeveloperControlled { readonly sdk: Web3Sdk; @@ -16,12 +35,25 @@ export class SparkWalletDeveloperControlled { /** * Creates a new Spark wallet associated with the current project. + * + * @param params - Wallet creation parameters + * @param params.tags - Optional tags to organize the wallet + * @param params.network - Network to create the wallet on (default: "REGTEST") + * @param params.purpose - Purpose of the wallet (legacy parameter, not used) + * @returns Promise that resolves to the created Spark wallet + * + * @example + * ```typescript + * const wallet = await sparkWallet.createWallet({ + * tags: ["tokenization", "mainnet"], + * network: "MAINNET" + * }); + * ``` */ async createWallet({ tags, network = "REGTEST", - purpose = "general", - }: { + }: { tags?: string[]; network?: "MAINNET" | "REGTEST"; purpose?: "tokenization" | "general"; @@ -33,17 +65,14 @@ export class SparkWalletDeveloperControlled { } // Generate mnemonic for Spark wallet - const mnemonic = await generateMnemonic(256); + const mnemonic = EmbeddedWallet.brew(256); const encryptedMnemonic = await encryptWithPublicKey({ publicKey: project.publicKey, - data: mnemonic, + data: mnemonic.join(" "), }); - // Initialize Spark wallet to get address and public key - // Dynamic import to avoid bundling Node.js dependencies - const { SparkWallet } = await import("@buildonspark/spark-sdk"); - const { wallet: sparkWallet } = await SparkWallet.initialize({ - mnemonicOrSeed: mnemonic, + const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic.join(" "), options: { network }, }); @@ -58,7 +87,6 @@ export class SparkWalletDeveloperControlled { sparkAddress, publicKey, network, - purpose, }; const { data, status } = await this.sdk.axiosInstance.post( @@ -74,7 +102,15 @@ export class SparkWalletDeveloperControlled { } /** - * Retrieves all Spark wallets for the project + * Retrieves all Spark wallets for the current project. + * + * @returns Promise that resolves to an array of all Spark wallets in the project + * + * @example + * ```typescript + * const wallets = await sparkWallet.getWallets(); + * console.log(`Found ${wallets.length} Spark wallets`); + * ``` */ async getWallets(): Promise { const { data, status } = await this.sdk.axiosInstance.get( @@ -89,14 +125,26 @@ export class SparkWalletDeveloperControlled { } /** - * Retrieves a specific Spark wallet by ID + * Retrieves a specific Spark wallet by ID and creates a wallet instance. + * + * @param walletId - The unique identifier of the wallet to retrieve + * @param decryptKey - Whether to decrypt and return the mnemonic key (default: false) + * @returns Promise that resolves to wallet info and initialized IssuerSparkWallet instance + * + * @throws {Error} When private key is not found or wallet retrieval fails + * + * @example + * ```typescript + * const { info, wallet } = await sparkWallet.getWallet("wallet-id-123"); + * const address = await wallet.getSparkAddress(); + * ``` */ async getWallet( walletId: string, decryptKey = false, ): Promise<{ info: Web3ProjectSparkWallet; - wallet: any; // SparkWallet type from @buildonspark/spark-sdk + wallet: IssuerSparkWallet; }> { if (this.sdk.privateKey === undefined) { throw new Error("Private key not found"); @@ -118,9 +166,7 @@ export class SparkWalletDeveloperControlled { web3Wallet.key = mnemonic; } - // Initialize Spark wallet with decrypted mnemonic - const { SparkWallet } = await import("@buildonspark/spark-sdk"); - const { wallet: sparkWallet } = await SparkWallet.initialize({ + const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ mnemonicOrSeed: mnemonic, options: { network: web3Wallet.network }, }); @@ -151,46 +197,203 @@ export class SparkWalletDeveloperControlled { } /** - * Get Spark wallets designated as token issuers + * Get token metadata for tokens created by a specific issuer wallet. + * + * @param walletId - The ID of the issuer wallet to get token metadata for + * @returns Promise that resolves to token metadata information + * + * @throws {Error} When token metadata retrieval fails + * + * @example + * ```typescript + * const metadata = await sparkWallet.getIssuerTokenMetadata("wallet-id"); + * console.log(`Token: ${metadata.tokenName} (${metadata.tokenSymbol})`); + * ``` */ - async getIssuerWallets(): Promise { - return this.getWalletsByTag("spark-issuer"); + async getIssuerTokenMetadata(walletId: string): Promise { + const { wallet } = await this.getWallet(walletId); + return await wallet.getIssuerTokenMetadata(); } /** - * Designate a Spark wallet as a token issuer + * Creates a new token using a specific issuer wallet. + * + * @param walletId - The ID of the issuer wallet to use for token creation + * @param params - Token creation parameters + * @param params.tokenName - The full name of the token + * @param params.tokenTicker - The ticker symbol for the token + * @param params.decimals - Number of decimal places for the token + * @param params.maxSupply - Maximum supply of tokens (optional, defaults to unlimited) + * @param params.isFreezable - Whether token transfers can be frozen + * @returns Promise that resolves to transaction information + * + * @throws {Error} When token creation fails + * + * @example + * ```typescript + * const result = await sparkWallet.createToken("wallet-id", { + * tokenName: "My Token", + * tokenTicker: "MTK", + * decimals: 8, + * maxSupply: "1000000", + * isFreezable: true + * }); + * console.log(`Token created with transaction: ${result.transactionId}`); + * ``` */ - async designateAsIssuer(walletId: string): Promise { - const { status } = await this.sdk.axiosInstance.post( - `api/project-wallet/${this.sdk.projectId}/spark/${walletId}/designate-issuer`, - ); - - if (status !== 200) { - throw new Error("Failed to designate wallet as issuer"); + async createToken( + walletId: string, + params: { + tokenName: string; + tokenTicker: string; + decimals: number; + maxSupply?: string; + isFreezable: boolean; } + ): Promise<{ transactionId: string }> { + const { wallet } = await this.getWallet(walletId); + + const transactionId = await wallet.createToken({ + tokenName: params.tokenName, + tokenTicker: params.tokenTicker, + decimals: params.decimals, + maxSupply: params.maxSupply ? BigInt(params.maxSupply) : undefined, + isFreezable: params.isFreezable, + }); + + return { + transactionId, + }; } + /** - * Remove issuer designation from a Spark wallet + * Mints tokens to the issuer wallet address. + * + * @param walletId - The ID of the issuer wallet to mint tokens to + * @param params - Minting parameters + * @param params.amount - The amount of tokens to mint (as string to handle large numbers) + * @returns Promise that resolves to transaction information + * + * @throws {Error} When token minting fails + * + * @example + * ```typescript + * const result = await sparkWallet.mintTokens("wallet-id", { + * amount: "1000000" + * }); + * console.log(`Minted tokens with transaction: ${result.transactionId}`); + * ``` */ - async removeIssuerDesignation(walletId: string): Promise { - const { status } = await this.sdk.axiosInstance.delete( - `api/project-wallet/${this.sdk.projectId}/spark/${walletId}/designate-issuer`, - ); - - if (status !== 200) { - throw new Error("Failed to remove issuer designation"); + async mintTokens( + walletId: string, + params: { + amount: string; } + ): Promise<{ transactionId: string }> { + const { wallet } = await this.getWallet(walletId); + + const result = await wallet.mintTokens(BigInt(params.amount)); + + return { + transactionId: result, + }; } + /** - * Check if project has any designated issuer wallets + * Transfers tokens from an issuer wallet to another Spark address. + * + * @param walletId - The ID of the issuer wallet to transfer tokens from + * @param params - Transfer parameters + * @param params.tokenIdentifier - The Bech32m token identifier for the token to transfer + * @param params.amount - The amount of tokens to transfer (as string to handle large numbers) + * @param params.toAddress - The recipient Spark address + * @returns Promise that resolves to transaction information + * + * @throws {Error} When token transfer fails + * + * @example + * ```typescript + * const result = await sparkWallet.transferTokens("wallet-id", { + * tokenIdentifier: "spark1abc...", + * amount: "100000", + * toAddress: "spark1def..." + * }); + * console.log(`Transferred tokens with transaction: ${result.transactionId}`); + * ``` */ - async hasIssuerWallet(): Promise<{ hasIssuer: boolean; walletId?: string }> { - const issuerWallets = await this.getIssuerWallets(); + async transferTokens( + walletId: string, + params: { + tokenIdentifier: Bech32mTokenIdentifier; + amount: string; + toAddress: string; + } + ): Promise<{ transactionId: string }> { + const { wallet } = await this.getWallet(walletId); + + const result = await wallet.transferTokens({ + tokenIdentifier: params.tokenIdentifier, + tokenAmount: BigInt(params.amount), + receiverSparkAddress: params.toAddress, + }); + return { - hasIssuer: issuerWallets.length > 0, - walletId: issuerWallets[0]?.id, + transactionId: result, }; } + + /** + * Retrieves metadata for tokens created by a specific issuer wallet. + * + * @param walletId - The ID of the issuer wallet to get token metadata for + * @returns Promise that resolves to token metadata information + * + * @throws {Error} When token metadata retrieval fails + * + * @example + * ```typescript + * const metadata = await sparkWallet.getCreatedTokens("wallet-id"); + * console.log(`Token: ${metadata.tokenName} (${metadata.tokenSymbol})`); + * ``` + */ + async getCreatedTokens(walletId: string): Promise { + const { wallet } = await this.getWallet(walletId); + return await wallet.getIssuerTokenMetadata(); + } + + /** + * Retrieves token balance for a specific address using Sparkscan API. + * + * @param params - Balance query parameters + * @param params.tokenId - The token identifier to check balance for + * @param params.address - The Spark address to check balance of + * @returns Promise that resolves to balance information + * + * @throws {Error} When balance retrieval fails + * + * @example + * ```typescript + * const balance = await sparkWallet.getTokenBalance({ + * tokenId: "spark1token123...", + * address: "spark1addr456..." + * }); + * console.log(`Balance: ${balance.balance} tokens`); + * ``` + */ + async getTokenBalance(params: { + tokenId: string; + address: string; + }): Promise<{ balance: string }> { + const { data, status } = await this.sdk.axiosInstance.get( + `api/spark/tokens/${params.tokenId}/balance?address=${params.address}` + ); + + if (status === 200) { + return data; + } + + throw new Error("Failed to get token balance"); + } } \ No newline at end of file diff --git a/src/types/core/index.ts b/src/types/core/index.ts index db793d7..b3243cd 100644 --- a/src/types/core/index.ts +++ b/src/types/core/index.ts @@ -44,7 +44,6 @@ export type Web3ProjectSparkWallet = { sparkAddress: string; publicKey: string; network: "MAINNET" | "REGTEST"; - purpose?: "tokenization" | "general"; }; export type Web3JWTBody = { From 484c1dcf45b162ce4f23e36e37de46c4d69d6611 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 14 Nov 2025 00:58:49 +0800 Subject: [PATCH 03/30] feat: refactor Spark wallet functionality and add new token management interfaces --- src/sdk/wallet-developer-controlled/index.ts | 2 - src/sdk/wallet-developer-controlled/spark.ts | 321 +++++++++++++++---- src/types/spark/dev-wallet.ts | 137 ++++++++ 3 files changed, 399 insertions(+), 61 deletions(-) create mode 100644 src/types/spark/dev-wallet.ts diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 3bda74d..7615bb1 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -30,7 +30,6 @@ export class WalletDeveloperControlled { options: { tags?: string[]; network?: "MAINNET" | "REGTEST"; // For Spark only - purpose?: "tokenization" | "general"; // For Spark only } = {} ): Promise { if (chain === "cardano") { @@ -39,7 +38,6 @@ export class WalletDeveloperControlled { return this.spark.createWallet({ tags: options.tags, network: options.network, - purpose: options.purpose, }); } diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index cd057de..f3b07e2 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -6,13 +6,28 @@ import { v4 as uuidv4 } from "uuid"; import { IssuerSparkWallet, IssuerTokenMetadata } from "@buildonspark/issuer-sdk"; import { bitcoin, EmbeddedWallet } from "@meshsdk/bitcoin"; import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; +import { + SparkTokenCreationParams, + SparkMintTokensParams, + SparkBatchMintParams, + SparkTransferTokensParams, + SparkTokenBalanceParams, + SparkFreezeTokensParams, + SparkUnfreezeTokensParams, + SparkTransactionResult, + SparkBatchMintResult, + SparkTokenBalanceResult, + SparkFreezeResult, + SparkFrozenAddressesResult, + PaginationParams, +} from "../../types/spark/dev-wallet"; /** * SparkWalletDeveloperControlled - Manages Spark-specific developer-controlled wallets - * + * * This class provides functionality for managing developer-controlled Spark wallets, * including wallet creation, token operations, and issuer wallet management. - * + * * @example * ```typescript * const sparkWallet = sdk.wallet.spark; @@ -35,13 +50,12 @@ export class SparkWalletDeveloperControlled { /** * Creates a new Spark wallet associated with the current project. - * + * * @param params - Wallet creation parameters * @param params.tags - Optional tags to organize the wallet * @param params.network - Network to create the wallet on (default: "REGTEST") - * @param params.purpose - Purpose of the wallet (legacy parameter, not used) * @returns Promise that resolves to the created Spark wallet - * + * * @example * ```typescript * const wallet = await sparkWallet.createWallet({ @@ -56,7 +70,6 @@ export class SparkWalletDeveloperControlled { }: { tags?: string[]; network?: "MAINNET" | "REGTEST"; - purpose?: "tokenization" | "general"; } = {}): Promise { const project = await this.sdk.getProject(); @@ -103,9 +116,9 @@ export class SparkWalletDeveloperControlled { /** * Retrieves all Spark wallets for the current project. - * + * * @returns Promise that resolves to an array of all Spark wallets in the project - * + * * @example * ```typescript * const wallets = await sparkWallet.getWallets(); @@ -126,13 +139,13 @@ export class SparkWalletDeveloperControlled { /** * Retrieves a specific Spark wallet by ID and creates a wallet instance. - * + * * @param walletId - The unique identifier of the wallet to retrieve * @param decryptKey - Whether to decrypt and return the mnemonic key (default: false) * @returns Promise that resolves to wallet info and initialized IssuerSparkWallet instance - * + * * @throws {Error} When private key is not found or wallet retrieval fails - * + * * @example * ```typescript * const { info, wallet } = await sparkWallet.getWallet("wallet-id-123"); @@ -198,12 +211,12 @@ export class SparkWalletDeveloperControlled { /** * Get token metadata for tokens created by a specific issuer wallet. - * + * * @param walletId - The ID of the issuer wallet to get token metadata for * @returns Promise that resolves to token metadata information - * + * * @throws {Error} When token metadata retrieval fails - * + * * @example * ```typescript * const metadata = await sparkWallet.getIssuerTokenMetadata("wallet-id"); @@ -217,7 +230,7 @@ export class SparkWalletDeveloperControlled { /** * Creates a new token using a specific issuer wallet. - * + * * @param walletId - The ID of the issuer wallet to use for token creation * @param params - Token creation parameters * @param params.tokenName - The full name of the token @@ -226,9 +239,9 @@ export class SparkWalletDeveloperControlled { * @param params.maxSupply - Maximum supply of tokens (optional, defaults to unlimited) * @param params.isFreezable - Whether token transfers can be frozen * @returns Promise that resolves to transaction information - * + * * @throws {Error} When token creation fails - * + * * @example * ```typescript * const result = await sparkWallet.createToken("wallet-id", { @@ -243,14 +256,8 @@ export class SparkWalletDeveloperControlled { */ async createToken( walletId: string, - params: { - tokenName: string; - tokenTicker: string; - decimals: number; - maxSupply?: string; - isFreezable: boolean; - } - ): Promise<{ transactionId: string }> { + params: SparkTokenCreationParams + ): Promise { const { wallet } = await this.getWallet(walletId); const transactionId = await wallet.createToken({ @@ -268,51 +275,136 @@ export class SparkWalletDeveloperControlled { /** - * Mints tokens to the issuer wallet address. - * - * @param walletId - The ID of the issuer wallet to mint tokens to + * Mints tokens to a specified address or to the issuer wallet if no address provided. + * + * When an address is provided, this method performs a two-step process: + * 1. Mints tokens to the issuer wallet + * 2. Transfers the minted tokens to the specified address + * + * @param walletId - The ID of the issuer wallet to mint tokens with * @param params - Minting parameters + * @param params.tokenization_id - The Bech32m token identifier to mint * @param params.amount - The amount of tokens to mint (as string to handle large numbers) - * @returns Promise that resolves to transaction information - * - * @throws {Error} When token minting fails - * + * @param params.address - Optional Spark address to receive tokens (defaults to issuer wallet) + * @returns Promise that resolves to transaction information (transfer tx if address provided, mint tx otherwise) + * + * @throws {Error} When token minting or transfer fails + * * @example * ```typescript - * const result = await sparkWallet.mintTokens("wallet-id", { + * // Mint to issuer wallet + * const result1 = await sparkWallet.mintTokens("wallet-id", { + * tokenization_id: "spark1token123...", * amount: "1000000" * }); - * console.log(`Minted tokens with transaction: ${result.transactionId}`); + * + * // Mint and transfer to specific address (two-step process) + * const result2 = await sparkWallet.mintTokens("wallet-id", { + * tokenization_id: "spark1token123...", + * amount: "1000000", + * address: "spark1recipient456..." + * }); * ``` */ async mintTokens( walletId: string, - params: { - amount: string; + params: SparkMintTokensParams + ): Promise { + const { wallet } = await this.getWallet(walletId); + + if (params.address) { + // Two-step process: mint to issuer, then transfer to target address + await wallet.mintTokens(BigInt(params.amount)); + + // Transfer the minted tokens to the specified address + const transferResult = await wallet.transferTokens({ + tokenIdentifier: params.tokenization_id, + tokenAmount: BigInt(params.amount), + receiverSparkAddress: params.address, + }); + + return { + transactionId: transferResult, + }; + } else { + // Direct mint to issuer wallet + const mintResult = await wallet.mintTokens(BigInt(params.amount)); + + return { + transactionId: mintResult, + }; } - ): Promise<{ transactionId: string }> { + } + + /** + * Efficiently mints tokens to multiple recipients in batch. + * + * This method optimizes the minting process by: + * 1. Calculating the total amount needed for all recipients + * 2. Performing a single mint operation to the issuer wallet + * 3. Executing a single batch transfer to all recipients simultaneously + * + * @param walletId - The ID of the issuer wallet to mint tokens with + * @param params - Batch minting parameters + * @param params.tokenization_id - The Bech32m token identifier to mint + * @param params.recipients - Array of recipient addresses and amounts + * @returns Promise that resolves to mint transaction ID and batch transfer transaction ID + * + * @throws {Error} When token minting or batch transfer fails + * + * @example + * ```typescript + * const result = await sparkWallet.batchMintTokens("wallet-id", { + * tokenization_id: "spark1token123...", + * recipients: [ + * { address: "spark1addr1...", amount: "1000" }, + * { address: "spark1addr2...", amount: "2000" }, + * { address: "spark1addr3...", amount: "500" } + * ] + * }); + * console.log(`Mint tx: ${result.mintTransactionId}`); + * console.log(`Batch transfer tx: ${result.batchTransferTransactionId}`); + * ``` + */ + async batchMintTokens( + walletId: string, + params: SparkBatchMintParams + ): Promise { const { wallet } = await this.getWallet(walletId); - const result = await wallet.mintTokens(BigInt(params.amount)); + // Calculate total amount needed for all recipients + const totalAmount = params.recipients.reduce( + (sum, recipient) => sum + BigInt(recipient.amount), + 0n + ); + + const mintTransactionId = await wallet.mintTokens(totalAmount); + const receiverOutputs = params.recipients.map(recipient => ({ + tokenIdentifier: params.tokenization_id, + tokenAmount: BigInt(recipient.amount), + receiverSparkAddress: recipient.address, + })); + + const batchTransferTransactionId = await wallet.batchTransferTokens(receiverOutputs); return { - transactionId: result, + mintTransactionId, + batchTransferTransactionId, }; } - /** * Transfers tokens from an issuer wallet to another Spark address. - * + * * @param walletId - The ID of the issuer wallet to transfer tokens from * @param params - Transfer parameters * @param params.tokenIdentifier - The Bech32m token identifier for the token to transfer * @param params.amount - The amount of tokens to transfer (as string to handle large numbers) * @param params.toAddress - The recipient Spark address * @returns Promise that resolves to transaction information - * + * * @throws {Error} When token transfer fails - * + * * @example * ```typescript * const result = await sparkWallet.transferTokens("wallet-id", { @@ -325,12 +417,8 @@ export class SparkWalletDeveloperControlled { */ async transferTokens( walletId: string, - params: { - tokenIdentifier: Bech32mTokenIdentifier; - amount: string; - toAddress: string; - } - ): Promise<{ transactionId: string }> { + params: SparkTransferTokensParams + ): Promise { const { wallet } = await this.getWallet(walletId); const result = await wallet.transferTokens({ @@ -346,12 +434,12 @@ export class SparkWalletDeveloperControlled { /** * Retrieves metadata for tokens created by a specific issuer wallet. - * + * * @param walletId - The ID of the issuer wallet to get token metadata for * @returns Promise that resolves to token metadata information - * + * * @throws {Error} When token metadata retrieval fails - * + * * @example * ```typescript * const metadata = await sparkWallet.getCreatedTokens("wallet-id"); @@ -365,14 +453,14 @@ export class SparkWalletDeveloperControlled { /** * Retrieves token balance for a specific address using Sparkscan API. - * + * * @param params - Balance query parameters * @param params.tokenId - The token identifier to check balance for * @param params.address - The Spark address to check balance of * @returns Promise that resolves to balance information - * + * * @throws {Error} When balance retrieval fails - * + * * @example * ```typescript * const balance = await sparkWallet.getTokenBalance({ @@ -382,10 +470,7 @@ export class SparkWalletDeveloperControlled { * console.log(`Balance: ${balance.balance} tokens`); * ``` */ - async getTokenBalance(params: { - tokenId: string; - address: string; - }): Promise<{ balance: string }> { + async getTokenBalance(params: SparkTokenBalanceParams): Promise { const { data, status } = await this.sdk.axiosInstance.get( `api/spark/tokens/${params.tokenId}/balance?address=${params.address}` ); @@ -396,4 +481,122 @@ export class SparkWalletDeveloperControlled { throw new Error("Failed to get token balance"); } + + /** + * Freezes all tokens at a specific Spark address (compliance/admin control). + * + * This operation can only be performed by issuer wallets on freezable tokens. + * Frozen tokens cannot be transferred until unfrozen by the issuer. + * + * @param walletId - The ID of the issuer wallet with freeze authority + * @param params - Freeze parameters + * @param params.address - The Spark address to freeze tokens at + * @returns Promise that resolves to freeze operation details + * + * @throws {Error} When token freezing fails or tokens are not freezable + * + * @example + * ```typescript + * const result = await sparkWallet.freezeTokens("issuer-wallet-id", { + * address: "spark1suspicious123..." + * }); + * console.log(`Frozen ${result.impactedTokenAmount} tokens at ${result.impactedOutputIds.length} outputs`); + * ``` + */ + async freezeTokens( + walletId: string, + params: SparkFreezeTokensParams + ): Promise { + const { wallet } = await this.getWallet(walletId); + + const result = await wallet.freezeTokens(params.address); + + return { + impactedOutputIds: result.impactedOutputIds, + impactedTokenAmount: result.impactedTokenAmount.toString(), + }; + } + + /** + * Unfreezes all tokens at a specific Spark address (compliance/admin control). + * + * This operation can only be performed by issuer wallets that previously froze the tokens. + * Unfrozen tokens can be transferred normally again. + * + * @param walletId - The ID of the issuer wallet with unfreeze authority + * @param params - Unfreeze parameters + * @param params.address - The Spark address to unfreeze tokens at + * @returns Promise that resolves to unfreeze operation details + * + * @throws {Error} When token unfreezing fails + * + * @example + * ```typescript + * const result = await sparkWallet.unfreezeTokens("issuer-wallet-id", { + * address: "spark1cleared123..." + * }); + * console.log(`Unfrozen ${result.impactedTokenAmount} tokens at ${result.impactedOutputIds.length} outputs`); + * ``` + */ + async unfreezeTokens( + walletId: string, + params: SparkUnfreezeTokensParams + ): Promise { + const { wallet } = await this.getWallet(walletId); + + const result = await wallet.unfreezeTokens(params.address); + + return { + impactedOutputIds: result.impactedOutputIds, + impactedTokenAmount: result.impactedTokenAmount.toString(), + }; + } + + /** + * Retrieves frozen addresses with pagination support for administrative review. + * + * This method queries the backend service to get a paginated list of addresses + * that currently have frozen tokens. Useful for compliance dashboards and admin tables. + * + * @param params - Optional pagination parameters + * @param params.page - Page number to retrieve (1-based, default: 1) + * @param params.limit - Number of items per page (default: 50) + * @param params.offset - Number of items to skip (alternative to page) + * @returns Promise that resolves to paginated list of frozen addresses with metadata + * + * @throws {Error} When frozen address query fails + * + * @example + * ```typescript + * // Get first page with default limit + * const frozenInfo = await sparkWallet.getFrozenAddresses(); + * + * // Get specific page with custom limit + * const page2 = await sparkWallet.getFrozenAddresses({ page: 2, limit: 25 }); + * + * // Display in admin table + * frozenInfo.frozenAddresses.forEach(addr => { + * console.log(`${addr.address}: ${addr.frozenTokenAmount} tokens frozen since ${addr.frozenAt}`); + * }); + * console.log(`Page ${frozenInfo.pagination.currentPage} of ${frozenInfo.pagination.totalPages}`); + * ``` + */ + async getFrozenAddresses(params?: PaginationParams): Promise { + const queryParams = new URLSearchParams({ + projectId: this.sdk.projectId, + ...(params?.page && { page: params.page.toString() }), + ...(params?.limit && { limit: params.limit.toString() }), + ...(params?.offset && { offset: params.offset.toString() }), + }); + + const { data, status } = await this.sdk.axiosInstance.get( + `api/spark/frozen-addresses?${queryParams.toString()}` + ); + + if (status === 200) { + return data as SparkFrozenAddressesResult; + } + + throw new Error("Failed to get frozen addresses"); + } } \ No newline at end of file diff --git a/src/types/spark/dev-wallet.ts b/src/types/spark/dev-wallet.ts new file mode 100644 index 0000000..e1d9427 --- /dev/null +++ b/src/types/spark/dev-wallet.ts @@ -0,0 +1,137 @@ +import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; + +/** + * Parameters for creating a new Spark token + */ +export interface SparkTokenCreationParams { + tokenName: string; + tokenTicker: string; + decimals: number; + maxSupply?: string; + isFreezable: boolean; +} + +/** + * Parameters for minting Spark tokens + */ +export interface SparkMintTokensParams { + tokenization_id: Bech32mTokenIdentifier; + amount: string; + address?: string; +} + +/** + * Individual recipient for batch minting operations + */ +export interface SparkBatchRecipient { + address: string; + amount: string; +} + +/** + * Parameters for batch minting Spark tokens to multiple recipients + */ +export interface SparkBatchMintParams { + tokenization_id: Bech32mTokenIdentifier; + recipients: SparkBatchRecipient[]; +} + +/** + * Parameters for transferring Spark tokens + */ +export interface SparkTransferTokensParams { + tokenIdentifier: Bech32mTokenIdentifier; + amount: string; + toAddress: string; +} + +/** + * Parameters for querying token balance + */ +export interface SparkTokenBalanceParams { + tokenId: string; + address: string; +} + +/** + * Standard transaction result for Spark operations + */ +export interface SparkTransactionResult { + transactionId: string; +} + +/** + * Result for batch minting operations + */ +export interface SparkBatchMintResult { + mintTransactionId: string; + batchTransferTransactionId: string; +} + +/** + * Result for token balance queries + */ +export interface SparkTokenBalanceResult { + balance: string; +} + +/** + * Parameters for freezing tokens at a specific address + */ +export interface SparkFreezeTokensParams { + address: string; +} + +/** + * Parameters for unfreezing tokens at a specific address + */ +export interface SparkUnfreezeTokensParams { + address: string; +} + +/** + * Result for freeze/unfreeze operations + */ +export interface SparkFreezeResult { + impactedOutputIds: string[]; + impactedTokenAmount: string; +} + +/** + * Information about a frozen address + */ +export interface SparkFrozenAddressInfo { + address: string; + frozenTokenAmount: string; + freezeTransactionId?: string; + frozenAt: string; +} + +/** + * Pagination parameters for queries + */ +export interface PaginationParams { + page?: number; + limit?: number; + offset?: number; +} + +/** + * Pagination metadata in results + */ +export interface PaginationMeta { + totalItems: number; + currentPage: number; + totalPages: number; + limit: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +} + +/** + * Result for querying frozen addresses with pagination + */ +export interface SparkFrozenAddressesResult { + frozenAddresses: SparkFrozenAddressInfo[]; + pagination: PaginationMeta; +} \ No newline at end of file From 46f8053a9b52bdb16bbb9b4d1ca2e31bf42c0552 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Sat, 15 Nov 2025 02:44:09 +0800 Subject: [PATCH 04/30] feat: implement multi-chain wallet support --- src/sdk/sponsorship/index.ts | 8 +- src/sdk/wallet-developer-controlled/index.ts | 293 ++++++++++++++----- src/sdk/wallet-developer-controlled/spark.ts | 11 +- src/types/core/index.ts | 2 + src/types/core/multi-chain.ts | 60 ++++ src/types/spark/dev-wallet.ts | 13 +- src/types/spark/index.ts | 2 +- 7 files changed, 289 insertions(+), 100 deletions(-) create mode 100644 src/types/core/multi-chain.ts diff --git a/src/sdk/sponsorship/index.ts b/src/sdk/sponsorship/index.ts index 5e3cd7d..014287c 100644 --- a/src/sdk/sponsorship/index.ts +++ b/src/sdk/sponsorship/index.ts @@ -340,10 +340,10 @@ export class Sponsorship { } private async getSponsorWallet(projectWalletId: string) { - const networkId = this.sdk.network === "mainnet" ? 1 : 0; - // For sponsorship, we assume Cardano wallets (since this is for Cardano sponsorship) - const wallet = await this.sdk.wallet.getWallet("cardano", projectWalletId, { networkId }); - return wallet.wallet; + const networkId: 0 | 1 = this.sdk.network === "mainnet" ? 1 : 0; + // For sponsorship, we use direct Cardano wallet access + const walletResult = await this.sdk.wallet.cardano.getWallet(projectWalletId, networkId); + return walletResult.wallet; } /** diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 7615bb1..c196a24 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -1,18 +1,46 @@ import { Web3Sdk } from ".."; -import { Web3ProjectCardanoWallet, Web3ProjectSparkWallet } from "../../types"; +import { + MultiChainWalletOptions, + MultiChainWalletInfo, + MultiChainWalletInstance, + SupportedChain, + ChainWalletOptions +} from "../../types/core/multi-chain"; import { CardanoWalletDeveloperControlled } from "./cardano"; import { SparkWalletDeveloperControlled } from "./spark"; +import { MeshWallet } from "@meshsdk/wallet"; +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import { deserializeBech32Address } from "@meshsdk/core-cst"; +import { encryptWithPublicKey, decryptWithPrivateKey } from "../../functions"; +import { v4 as uuidv4 } from "uuid"; -// Export chain-specific classes +// Export chain-specific classes for advanced usage export { CardanoWalletDeveloperControlled } from "./cardano"; export { SparkWalletDeveloperControlled } from "./spark"; /** * The `WalletDeveloperControlled` class provides functionality for managing developer-controlled wallets - * within a Web3 project. It orchestrates chain-specific wallet management for Cardano and Spark. + * within a Web3 project. Supports multi-chain wallets with UTXO-native design. + * + * @example + * ```typescript + * // Create multi-chain wallet + * const walletInfo = await sdk.wallet.createWallet({ + * tags: ["minting"], + * networks: { cardano: 1, spark: 1 } + * }); + * + * // Get specific chain for performance + * const { sparkWallet } = await sdk.wallet.getWallet(walletInfo.id, 1, "spark"); + * + * // Get all chains + * const { cardanoWallet, sparkWallet } = await sdk.wallet.getWallet(walletInfo.id, 1); + * ``` */ export class WalletDeveloperControlled { readonly sdk: Web3Sdk; + + // Chain-specific handlers (public access for direct operations) readonly cardano: CardanoWalletDeveloperControlled; readonly spark: SparkWalletDeveloperControlled; @@ -23,100 +51,213 @@ export class WalletDeveloperControlled { } /** - * Creates a new wallet for the specified chain + * Creates a new multi-chain wallet associated with the current project. + * One wallet per project with unified ID containing all chain keys. + * + * @param options - Multi-chain wallet creation options + * @param options.tags - Optional tags to organize the wallet + * @param options.networks - Network configuration for each chain + * @returns Promise that resolves to multi-chain wallet information + * + * @throws {Error} When wallet creation fails or project has existing wallet + * + * @example + * ```typescript + * const wallet = await sdk.wallet.createWallet({ + * tags: ["tokenization", "mainnet"], + * networkId: 1, // Single network for all chains + * chains: ["cardano", "spark"] + * }); + * ``` */ - async createWallet( - chain: "cardano" | "spark", - options: { - tags?: string[]; - network?: "MAINNET" | "REGTEST"; // For Spark only - } = {} - ): Promise { - if (chain === "cardano") { - return this.cardano.createWallet({ tags: options.tags }); - } else if (chain === "spark") { - return this.spark.createWallet({ - tags: options.tags, - network: options.network, + async createWallet(options: MultiChainWalletOptions = {}): Promise { + // Check if project already has a wallet + const existingWallet = await this.getProjectWallet().catch(() => null); + if (existingWallet) { + throw new Error("Project already has a wallet. Use getWallet() to retrieve it or add chains to existing wallet."); + } + + const project = await this.sdk.getProject(); + if (!project.publicKey) { + throw new Error("Project public key not found"); + } + + const walletId = uuidv4(); + const networkId = options.networkId || 1; // Default to mainnet + const enabledChains = options.chains || ["cardano", "spark"]; // Default chains + const chains: MultiChainWalletInfo['chains'] = {}; + + // Generate single mnemonic for all chains (matches user-controlled pattern) + const sharedMnemonic = MeshWallet.brew() as string[]; + const encryptedKey = await encryptWithPublicKey({ + publicKey: project.publicKey, + data: sharedMnemonic.join(" "), + }); + + // Generate addresses for enabled chains using shared mnemonic and network + if (enabledChains.includes("cardano")) { + const tempWallet = new MeshWallet({ + networkId: networkId, + key: { type: "mnemonic", words: sharedMnemonic }, + fetcher: this.sdk.providerFetcher, + submitter: this.sdk.providerSubmitter, + }); + await tempWallet.init(); + + const addresses = await tempWallet.getAddresses(); + const { pubKeyHash, stakeCredentialHash } = deserializeBech32Address(addresses.baseAddressBech32!); + + chains.cardano = { + pubKeyHash, + stakeCredentialHash, + address: addresses.baseAddressBech32!, + }; + } + + if (enabledChains.includes("spark")) { + const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; + const { wallet: tempSparkWallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: sharedMnemonic.join(" "), + options: { network: sparkNetwork }, }); + + const sparkAddress = await tempSparkWallet.getSparkAddress(); + const publicKey = await tempSparkWallet.getIdentityPublicKey(); + + chains.spark = { + sparkAddress, + publicKey, + }; } - - throw new Error(`Unsupported chain: ${chain}`); + + // Store unified wallet in database + const walletData = { + id: walletId, + projectId: this.sdk.projectId, + tags: options.tags || [], + key: encryptedKey, // Single shared mnemonic + networkId: networkId, // Single network for all chains + chains, + createdAt: new Date().toISOString(), + }; + + const { data, status } = await this.sdk.axiosInstance.post( + `api/project-wallet/multi-chain`, + walletData + ); + + if (status === 200) { + return data as MultiChainWalletInfo; + } + + throw new Error("Failed to create multi-chain wallet"); } /** - * Get wallets for a specific chain + * Retrieves a multi-chain wallet with optional chain-specific loading for performance. + * + * @param walletId - The unique identifier of the wallet + * @param networkId - Network ID (0 = testnet, 1 = mainnet) + * @param chain - Optional specific chain to load (performance optimization) + * @param options - Additional chain-specific options + * @returns Promise that resolves to multi-chain wallet instance + * + * @example + * ```typescript + * // Load specific chain for performance + * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); + * + * // Load all available chains + * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id"); + * ``` */ - async getWallets( - chain: "cardano" | "spark" - ): Promise { - if (chain === "cardano") { - return this.cardano.getWallets(); - } else if (chain === "spark") { - return this.spark.getWallets(); + async getWallet( + walletId: string, + chain?: SupportedChain, + options: ChainWalletOptions = {} + ): Promise { + // Get the unified wallet data first + const walletInfo = await this.getProjectWallet(); + + const instance: MultiChainWalletInstance = { + info: walletInfo + }; + + // Decrypt shared mnemonic once (used by all chains) + let sharedMnemonic: string | null = null; + if (this.sdk.privateKey) { + sharedMnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: walletInfo.key, + }); + } + + // Load requested chain or ALL available chains if no specific chain requested + if ((chain === "cardano" || !chain) && walletInfo.chains.cardano && sharedMnemonic) { + const cardanoWallet = new MeshWallet({ + networkId: walletInfo.networkId, // Use wallet's stored network + key: { type: "mnemonic", words: sharedMnemonic.split(" ") }, + fetcher: this.sdk.providerFetcher, + submitter: this.sdk.providerSubmitter, + }); + await cardanoWallet.init(); + + instance.cardanoWallet = cardanoWallet; + } + + if ((chain === "spark" || !chain) && walletInfo.chains.spark && sharedMnemonic) { + const sparkNetwork = walletInfo.networkId === 1 ? "MAINNET" : "REGTEST"; + const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: sharedMnemonic, + options: { network: sparkNetwork }, + }); + + instance.sparkWallet = sparkWallet; } - - throw new Error(`Unsupported chain: ${chain}`); + + // Future: Bitcoin wallet loading + // if ((chain === "bitcoin" || !chain) && walletInfo.chains.bitcoin && sharedMnemonic) { ... } + + return instance; } /** - * Get a specific wallet by ID and chain + * Get the project's single multi-chain wallet */ - async getWallet( - chain: "cardano" | "spark", - walletId: string, - options: { - networkId?: 0 | 1; // For Cardano only - decryptKey?: boolean; - } = {} - ): Promise { - if (chain === "cardano") { - return this.cardano.getWallet( - walletId, - options.networkId ?? 1, - options.decryptKey - ); - } else if (chain === "spark") { - return this.spark.getWallet(walletId, options.decryptKey); + async getProjectWallet(): Promise { + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/multi-chain/${this.sdk.projectId}` + ); + + if (status === 200) { + return data as MultiChainWalletInfo; } - - throw new Error(`Unsupported chain: ${chain}`); + + throw new Error("Project wallet not found"); } /** - * Get all wallets (both Cardano and Spark) for the project + * Helper method to check if wallet has Cardano chain */ - async getAllWallets(): Promise<{ - cardano: Web3ProjectCardanoWallet[]; - spark: Web3ProjectSparkWallet[]; - }> { - const [cardanoWallets, sparkWallets] = await Promise.all([ - this.cardano.getWallets(), - this.spark.getWallets(), - ]); - - return { - cardano: cardanoWallets, - spark: sparkWallets, - }; + private async hasCardanoWallet(walletId: string): Promise { + try { + const wallet = await this.getProjectWallet(); + return !!wallet.chains.cardano; + } catch { + return false; + } } /** - * Get wallets by tag across all chains + * Helper method to check if wallet has Spark chain */ - async getWalletsByTag(tag: string): Promise<{ - cardano: Web3ProjectCardanoWallet[]; - spark: Web3ProjectSparkWallet[]; - }> { - const [cardanoWallets, sparkWallets] = await Promise.all([ - this.cardano.getWalletsByTag(tag), - this.spark.getWalletsByTag(tag), - ]); - - return { - cardano: cardanoWallets, - spark: sparkWallets, - }; + private async hasSparkWallet(walletId: string): Promise { + try { + const wallet = await this.getProjectWallet(); + return !!wallet.chains.spark; + } catch { + return false; + } } } diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index f3b07e2..8277222 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -1,13 +1,10 @@ import { Web3Sdk } from ".."; import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; -import { Web3ProjectSparkWallet } from "../../types"; -import { generateMnemonic } from "@meshsdk/common"; +import { Web3ProjectSparkWallet, TokenCreationParams } from "../../types"; import { v4 as uuidv4 } from "uuid"; import { IssuerSparkWallet, IssuerTokenMetadata } from "@buildonspark/issuer-sdk"; -import { bitcoin, EmbeddedWallet } from "@meshsdk/bitcoin"; -import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; +import { EmbeddedWallet } from "@meshsdk/bitcoin"; import { - SparkTokenCreationParams, SparkMintTokensParams, SparkBatchMintParams, SparkTransferTokensParams, @@ -256,7 +253,7 @@ export class SparkWalletDeveloperControlled { */ async createToken( walletId: string, - params: SparkTokenCreationParams + params: TokenCreationParams ): Promise { const { wallet } = await this.getWallet(walletId); @@ -269,7 +266,7 @@ export class SparkWalletDeveloperControlled { }); return { - transactionId, + transactionId, // Returns transaction ID as proof of creation }; } diff --git a/src/types/core/index.ts b/src/types/core/index.ts index b3243cd..d1eed9f 100644 --- a/src/types/core/index.ts +++ b/src/types/core/index.ts @@ -76,3 +76,5 @@ export type SponsorshipTxParserPostRequestBody = { sponsorUtxo: string; network: "mainnet" | "testnet"; }; + +export * from "./multi-chain"; diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts new file mode 100644 index 0000000..84a0232 --- /dev/null +++ b/src/types/core/multi-chain.ts @@ -0,0 +1,60 @@ +import { MeshWallet } from "@meshsdk/wallet"; +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; + +/** + * Standardized network ID type (0 = testnet, 1 = mainnet) + */ +export type NetworkId = 0 | 1; + +/** + * Multi-chain wallet creation options + */ +export interface MultiChainWalletOptions { + tags?: string[]; + networkId?: NetworkId; + chains?: ("cardano" | "spark" | "bitcoin")[]; +} + +/** + * Multi-chain wallet information - one wallet per project with all chain keys + */ +export interface MultiChainWalletInfo { + id: string; + projectId: string; + tags: string[]; + key: string; + networkId: NetworkId; + chains: { + cardano?: { + pubKeyHash: string; + stakeCredentialHash: string; + address: string; + }; + spark?: { + sparkAddress: string; + publicKey: string; + }; + }; + createdAt: string; +} + +/** + * Multi-chain wallet instance with initialized wallet objects + */ +export interface MultiChainWalletInstance { + info: MultiChainWalletInfo; + cardanoWallet?: MeshWallet; + sparkWallet?: IssuerSparkWallet; +} + +/** + * Supported chain types for wallet operations + */ +export type SupportedChain = "cardano" | "spark"; + +/** + * Chain-specific wallet retrieval options + */ +export interface ChainWalletOptions { + decryptKey?: boolean; +} \ No newline at end of file diff --git a/src/types/spark/dev-wallet.ts b/src/types/spark/dev-wallet.ts index e1d9427..a34b6f0 100644 --- a/src/types/spark/dev-wallet.ts +++ b/src/types/spark/dev-wallet.ts @@ -1,18 +1,7 @@ import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; /** - * Parameters for creating a new Spark token - */ -export interface SparkTokenCreationParams { - tokenName: string; - tokenTicker: string; - decimals: number; - maxSupply?: string; - isFreezable: boolean; -} - -/** - * Parameters for minting Spark tokens + * Enhanced minting parameters for dev-controlled wallets (extends base MintTokenParams) */ export interface SparkMintTokensParams { tokenization_id: Bech32mTokenIdentifier; diff --git a/src/types/spark/index.ts b/src/types/spark/index.ts index be97025..2132d4a 100644 --- a/src/types/spark/index.ts +++ b/src/types/spark/index.ts @@ -237,7 +237,7 @@ export interface TokenCreationParams { tokenName: string; tokenTicker: string; decimals: number; - maxSupply: number; + maxSupply?: string; isFreezable: boolean; } From 9ea13b0b1c38684586b6c4a31f36e25a89aba0f5 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Mon, 17 Nov 2025 14:11:13 +0800 Subject: [PATCH 05/30] refactor: clean up wallet retrieval options and improve documentation --- src/sdk/wallet-developer-controlled/index.ts | 26 ++++++-------------- src/types/core/multi-chain.ts | 7 ------ 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index c196a24..fa7ab0c 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -3,8 +3,7 @@ import { MultiChainWalletOptions, MultiChainWalletInfo, MultiChainWalletInstance, - SupportedChain, - ChainWalletOptions + SupportedChain } from "../../types/core/multi-chain"; import { CardanoWalletDeveloperControlled } from "./cardano"; import { SparkWalletDeveloperControlled } from "./spark"; @@ -13,8 +12,6 @@ import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; import { deserializeBech32Address } from "@meshsdk/core-cst"; import { encryptWithPublicKey, decryptWithPrivateKey } from "../../functions"; import { v4 as uuidv4 } from "uuid"; - -// Export chain-specific classes for advanced usage export { CardanoWalletDeveloperControlled } from "./cardano"; export { SparkWalletDeveloperControlled } from "./spark"; @@ -154,36 +151,32 @@ export class WalletDeveloperControlled { } /** - * Retrieves a multi-chain wallet with optional chain-specific loading for performance. - * + * Retrieves a multi-chain wallet with optional chain-specific loading. + * * @param walletId - The unique identifier of the wallet * @param networkId - Network ID (0 = testnet, 1 = mainnet) * @param chain - Optional specific chain to load (performance optimization) * @param options - Additional chain-specific options * @returns Promise that resolves to multi-chain wallet instance - * + * * @example * ```typescript - * // Load specific chain for performance + * // Load specific chain * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); - * + * * // Load all available chains * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id"); * ``` */ async getWallet( - walletId: string, chain?: SupportedChain, - options: ChainWalletOptions = {} ): Promise { - // Get the unified wallet data first const walletInfo = await this.getProjectWallet(); const instance: MultiChainWalletInstance = { info: walletInfo }; - // Decrypt shared mnemonic once (used by all chains) let sharedMnemonic: string | null = null; if (this.sdk.privateKey) { sharedMnemonic = await decryptWithPrivateKey({ @@ -195,7 +188,7 @@ export class WalletDeveloperControlled { // Load requested chain or ALL available chains if no specific chain requested if ((chain === "cardano" || !chain) && walletInfo.chains.cardano && sharedMnemonic) { const cardanoWallet = new MeshWallet({ - networkId: walletInfo.networkId, // Use wallet's stored network + networkId: walletInfo.networkId, key: { type: "mnemonic", words: sharedMnemonic.split(" ") }, fetcher: this.sdk.providerFetcher, submitter: this.sdk.providerSubmitter, @@ -215,9 +208,6 @@ export class WalletDeveloperControlled { instance.sparkWallet = sparkWallet; } - // Future: Bitcoin wallet loading - // if ((chain === "bitcoin" || !chain) && walletInfo.chains.bitcoin && sharedMnemonic) { ... } - return instance; } @@ -226,7 +216,7 @@ export class WalletDeveloperControlled { */ async getProjectWallet(): Promise { const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/multi-chain/${this.sdk.projectId}` + `api/project-wallet/${this.sdk.projectId}` ); if (status === 200) { diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index 84a0232..494de99 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -51,10 +51,3 @@ export interface MultiChainWalletInstance { * Supported chain types for wallet operations */ export type SupportedChain = "cardano" | "spark"; - -/** - * Chain-specific wallet retrieval options - */ -export interface ChainWalletOptions { - decryptKey?: boolean; -} \ No newline at end of file From c5243640a19573ad67df350c6f690fcbc15cef6d Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Mon, 17 Nov 2025 21:21:20 +0800 Subject: [PATCH 06/30] refactor: wallet chain checks and update tokenization parameter naming --- src/sdk/wallet-developer-controlled/index.ts | 19 +-- src/sdk/wallet-developer-controlled/spark.ts | 116 ++++++++++++++----- src/types/core/multi-chain.ts | 2 +- src/types/spark/dev-wallet.ts | 4 +- 4 files changed, 94 insertions(+), 47 deletions(-) diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index fa7ab0c..b60f2f0 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -227,27 +227,14 @@ export class WalletDeveloperControlled { } /** - * Helper method to check if wallet has Cardano chain + * Helper method to check if wallet has the supported chain */ - private async hasCardanoWallet(walletId: string): Promise { + private async hasWallet(walletId: string, chain: SupportedChain): Promise { try { const wallet = await this.getProjectWallet(); - return !!wallet.chains.cardano; + return !!wallet.chains[chain]; } catch { return false; } } - - /** - * Helper method to check if wallet has Spark chain - */ - private async hasSparkWallet(walletId: string): Promise { - try { - const wallet = await this.getProjectWallet(); - return !!wallet.chains.spark; - } catch { - return false; - } - } - } diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index 8277222..d215d34 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -251,13 +251,14 @@ export class SparkWalletDeveloperControlled { * console.log(`Token created with transaction: ${result.transactionId}`); * ``` */ - async createToken( - walletId: string, - params: TokenCreationParams - ): Promise { - const { wallet } = await this.getWallet(walletId); + async createToken(params: TokenCreationParams): Promise { + const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); - const transactionId = await wallet.createToken({ + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } + + const transactionId = await sparkWallet.createToken({ tokenName: params.tokenName, tokenTicker: params.tokenTicker, decimals: params.decimals, @@ -266,7 +267,7 @@ export class SparkWalletDeveloperControlled { }); return { - transactionId, // Returns transaction ID as proof of creation + transactionId, }; } @@ -280,7 +281,7 @@ export class SparkWalletDeveloperControlled { * * @param walletId - The ID of the issuer wallet to mint tokens with * @param params - Minting parameters - * @param params.tokenization_id - The Bech32m token identifier to mint + * @param params.tokenizationId - The Bech32m token identifier to mint * @param params.amount - The amount of tokens to mint (as string to handle large numbers) * @param params.address - Optional Spark address to receive tokens (defaults to issuer wallet) * @returns Promise that resolves to transaction information (transfer tx if address provided, mint tx otherwise) @@ -290,43 +291,43 @@ export class SparkWalletDeveloperControlled { * @example * ```typescript * // Mint to issuer wallet - * const result1 = await sparkWallet.mintTokens("wallet-id", { + * const result1 = await sparkWallet.mintTokens({ * tokenization_id: "spark1token123...", * amount: "1000000" * }); * * // Mint and transfer to specific address (two-step process) - * const result2 = await sparkWallet.mintTokens("wallet-id", { + * const result2 = await sparkWallet.mintTokens({ * tokenization_id: "spark1token123...", * amount: "1000000", * address: "spark1recipient456..." * }); * ``` */ - async mintTokens( - walletId: string, - params: SparkMintTokensParams - ): Promise { - const { wallet } = await this.getWallet(walletId); + async mintTokens(params: SparkMintTokensParams): Promise { + const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } if (params.address) { // Two-step process: mint to issuer, then transfer to target address - await wallet.mintTokens(BigInt(params.amount)); - + await sparkWallet.mintTokens(BigInt(params.amount)); + // Transfer the minted tokens to the specified address - const transferResult = await wallet.transferTokens({ - tokenIdentifier: params.tokenization_id, + const transferResult = await sparkWallet.transferTokens({ + tokenIdentifier: params.tokenizationId, tokenAmount: BigInt(params.amount), receiverSparkAddress: params.address, }); - + return { transactionId: transferResult, }; } else { - // Direct mint to issuer wallet - const mintResult = await wallet.mintTokens(BigInt(params.amount)); - + const mintResult = await sparkWallet.mintTokens(BigInt(params.amount)); + return { transactionId: mintResult, }; @@ -343,7 +344,7 @@ export class SparkWalletDeveloperControlled { * * @param walletId - The ID of the issuer wallet to mint tokens with * @param params - Batch minting parameters - * @param params.tokenization_id - The Bech32m token identifier to mint + * @param params.tokenizationId - The Bech32m token identifier to mint * @param params.recipients - Array of recipient addresses and amounts * @returns Promise that resolves to mint transaction ID and batch transfer transaction ID * @@ -352,7 +353,7 @@ export class SparkWalletDeveloperControlled { * @example * ```typescript * const result = await sparkWallet.batchMintTokens("wallet-id", { - * tokenization_id: "spark1token123...", + * tokenizationId: "spark1token123...", * recipients: [ * { address: "spark1addr1...", amount: "1000" }, * { address: "spark1addr2...", amount: "2000" }, @@ -377,7 +378,7 @@ export class SparkWalletDeveloperControlled { const mintTransactionId = await wallet.mintTokens(totalAmount); const receiverOutputs = params.recipients.map(recipient => ({ - tokenIdentifier: params.tokenization_id, + tokenIdentifier: params.tokenizationId, tokenAmount: BigInt(recipient.amount), receiverSparkAddress: recipient.address, })); @@ -479,6 +480,65 @@ export class SparkWalletDeveloperControlled { throw new Error("Failed to get token balance"); } + /** + * Get metadata for a specific token. + * + * @param params - Token metadata query parameters + * @param params.tokenId - The token identifier to get metadata for + * @returns Promise that resolves to token metadata + * + * @example + * ```typescript + * const metadata = await sparkWallet.getTokenMetadata({ tokenId: "spark1token123..." }); + * console.log(`${metadata.name} (${metadata.ticker})`); + * ``` + */ + async getTokenMetadata(params: { tokenId: string }): Promise { + const { data, status } = await this.sdk.axiosInstance.post( + `api/spark/tokens/metadata`, + { + token_addresses: [params.tokenId] + } + ); + + if (status === 200) { + return data.metadata?.[0] || null; + } + + throw new Error("Failed to get token metadata"); + } + + /** + * Burns tokens permanently from circulation. + * + * @param params - Token burning parameters + * @param params.amount - The amount of tokens to burn from issuer wallet + * @returns Promise that resolves to transaction information + * + * @throws {Error} When token burning fails + * + * @example + * ```typescript + * const result = await sparkWallet.burnTokens({ + * amount: "1000" + * }); + * console.log(`Burned tokens with transaction: ${result.transactionId}`); + * ``` + */ + async burnTokens(params: { amount: string }): Promise { + const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } + + const result = await sparkWallet.burnTokens(BigInt(params.amount)); + + return { + transactionId: result, + }; + } + /** * Freezes all tokens at a specific Spark address (compliance/admin control). * @@ -567,10 +627,10 @@ export class SparkWalletDeveloperControlled { * ```typescript * // Get first page with default limit * const frozenInfo = await sparkWallet.getFrozenAddresses(); - * + * * // Get specific page with custom limit * const page2 = await sparkWallet.getFrozenAddresses({ page: 2, limit: 25 }); - * + * * // Display in admin table * frozenInfo.frozenAddresses.forEach(addr => { * console.log(`${addr.address}: ${addr.frozenTokenAmount} tokens frozen since ${addr.frozenAt}`); diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index 494de99..be02b00 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -12,7 +12,7 @@ export type NetworkId = 0 | 1; export interface MultiChainWalletOptions { tags?: string[]; networkId?: NetworkId; - chains?: ("cardano" | "spark" | "bitcoin")[]; + chains?: ("cardano" | "spark")[]; } /** diff --git a/src/types/spark/dev-wallet.ts b/src/types/spark/dev-wallet.ts index a34b6f0..dd2aba5 100644 --- a/src/types/spark/dev-wallet.ts +++ b/src/types/spark/dev-wallet.ts @@ -4,7 +4,7 @@ import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; * Enhanced minting parameters for dev-controlled wallets (extends base MintTokenParams) */ export interface SparkMintTokensParams { - tokenization_id: Bech32mTokenIdentifier; + tokenizationId: Bech32mTokenIdentifier; amount: string; address?: string; } @@ -21,7 +21,7 @@ export interface SparkBatchRecipient { * Parameters for batch minting Spark tokens to multiple recipients */ export interface SparkBatchMintParams { - tokenization_id: Bech32mTokenIdentifier; + tokenizationId: Bech32mTokenIdentifier; recipients: SparkBatchRecipient[]; } From f8bba3f1d4e47b03030f79cd5d6b767f1af30441 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Tue, 18 Nov 2025 03:08:07 +0800 Subject: [PATCH 07/30] feat: add Cardano token operations and metadata handling; enhance Spark token metadata queries --- .../wallet-developer-controlled/cardano.ts | 136 +++++++++++++++++- src/sdk/wallet-developer-controlled/index.ts | 42 +++--- src/sdk/wallet-developer-controlled/spark.ts | 32 ++++- src/types/cardano/dev-wallet.ts | 76 ++++++++++ src/types/core/multi-chain.ts | 2 - src/types/spark/dev-wallet.ts | 27 ++++ 6 files changed, 282 insertions(+), 33 deletions(-) create mode 100644 src/types/cardano/dev-wallet.ts diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index 753ad75..040fedf 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -1,9 +1,20 @@ import { Web3Sdk } from ".."; import { MeshWallet } from "@meshsdk/wallet"; import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; -import { Web3ProjectCardanoWallet } from "../../types"; +import { Web3ProjectCardanoWallet, TokenCreationParams } from "../../types"; import { deserializeBech32Address } from "@meshsdk/core-cst"; import { v4 as uuidv4 } from "uuid"; +import { + CardanoTransactionResult, + CardanoTokenBalanceParams, + CardanoTokenBalanceResult, + CardanoTransferTokensParams, + CardanoBatchTransferParams, + CardanoFreezeTokensParams, + CardanoUnfreezeTokensParams, + CardanoFreezeResult, + CardanoBurnTokensParams, +} from "../../types/cardano/dev-wallet"; /** * CardanoWalletDeveloperControlled - Manages Cardano-specific developer-controlled wallets @@ -152,4 +163,127 @@ export class CardanoWalletDeveloperControlled { throw new Error("Failed to get Cardano wallets by tag"); } + + // ================================================================================= + // TOKEN OPERATIONS - UNIMPLEMENTED (Future CIP-113 Integration) + // ================================================================================= + + /** + * Creates a new Cardano token (CIP-113 programmable tokens). + * + * @param params - Token creation parameters (same interface as Spark) + * @returns Promise that resolves to transaction information + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + * + * @example + * ```typescript + * const result = await cardanoWallet.createToken({ + * tokenName: "My Token", + * tokenTicker: "MTK", + * decimals: 6, + * maxSupply: "1000000", + * isFreezable: true + * }); + * ``` + */ + async createToken(params: TokenCreationParams): Promise { + throw new Error("Cardano token creation not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Mints Cardano tokens to specified address or issuer wallet. + * + * @param params - Minting parameters + * @returns Promise that resolves to transaction information + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async mintTokens(params: { tokenId: string; amount: string; address?: string }): Promise { + throw new Error("Cardano token minting not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Transfers Cardano tokens between addresses. + * + * @param params - Transfer parameters + * @returns Promise that resolves to transaction information + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async transferTokens(params: CardanoTransferTokensParams): Promise { + throw new Error("Cardano token transfer not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Batch transfers Cardano tokens to multiple recipients. + * + * @param params - Batch transfer parameters + * @returns Promise that resolves to transaction information + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async batchTransferTokens(params: CardanoBatchTransferParams): Promise { + throw new Error("Cardano batch token transfer not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Gets token balance for a Cardano address. + * + * @param params - Balance query parameters + * @returns Promise that resolves to balance information + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async getTokenBalance(params: CardanoTokenBalanceParams): Promise { + throw new Error("Cardano token balance query not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Gets metadata for a specific Cardano token. + * + * @param params - Token metadata query parameters + * @returns Promise that resolves to token metadata + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async getTokenMetadata(params: { tokenId: string }): Promise { + throw new Error("Cardano token metadata query not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Burns Cardano tokens permanently from circulation. + * + * @param params - Token burning parameters + * @returns Promise that resolves to transaction information + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async burnTokens(params: CardanoBurnTokensParams): Promise { + throw new Error("Cardano token burning not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Freezes Cardano tokens at specific address (CIP-113 compliance). + * + * @param params - Freeze parameters + * @returns Promise that resolves to freeze operation details + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async freezeTokens(params: CardanoFreezeTokensParams): Promise { + throw new Error("Cardano token freezing not implemented yet - awaiting CIP-113 standard"); + } + + /** + * Unfreezes Cardano tokens at specific address (CIP-113 compliance). + * + * @param params - Unfreeze parameters + * @returns Promise that resolves to unfreeze operation details + * + * @throws {Error} Method not implemented yet - awaiting CIP-113 + */ + async unfreezeTokens(params: CardanoUnfreezeTokensParams): Promise { + throw new Error("Cardano token unfreezing not implemented yet - awaiting CIP-113 standard"); + } } \ No newline at end of file diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index b60f2f0..1781e0c 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -18,18 +18,18 @@ export { SparkWalletDeveloperControlled } from "./spark"; /** * The `WalletDeveloperControlled` class provides functionality for managing developer-controlled wallets * within a Web3 project. Supports multi-chain wallets with UTXO-native design. - * + * * @example * ```typescript * // Create multi-chain wallet - * const walletInfo = await sdk.wallet.createWallet({ - * tags: ["minting"], - * networks: { cardano: 1, spark: 1 } + * const walletInfo = await sdk.wallet.createWallet({ + * tags: ["minting"], + * networks: { cardano: 1, spark: 1 } * }); - * + * * // Get specific chain for performance * const { sparkWallet } = await sdk.wallet.getWallet(walletInfo.id, 1, "spark"); - * + * * // Get all chains * const { cardanoWallet, sparkWallet } = await sdk.wallet.getWallet(walletInfo.id, 1); * ``` @@ -38,26 +38,26 @@ export class WalletDeveloperControlled { readonly sdk: Web3Sdk; // Chain-specific handlers (public access for direct operations) - readonly cardano: CardanoWalletDeveloperControlled; - readonly spark: SparkWalletDeveloperControlled; + readonly cardanoWallet: CardanoWalletDeveloperControlled; + readonly sparkWallet: SparkWalletDeveloperControlled; constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; - this.cardano = new CardanoWalletDeveloperControlled({ sdk }); - this.spark = new SparkWalletDeveloperControlled({ sdk }); + this.cardanoWallet = new CardanoWalletDeveloperControlled({ sdk }); + this.sparkWallet = new SparkWalletDeveloperControlled({ sdk }); } /** * Creates a new multi-chain wallet associated with the current project. * One wallet per project with unified ID containing all chain keys. - * + * * @param options - Multi-chain wallet creation options * @param options.tags - Optional tags to organize the wallet * @param options.networks - Network configuration for each chain * @returns Promise that resolves to multi-chain wallet information - * + * * @throws {Error} When wallet creation fails or project has existing wallet - * + * * @example * ```typescript * const wallet = await sdk.wallet.createWallet({ @@ -80,8 +80,8 @@ export class WalletDeveloperControlled { } const walletId = uuidv4(); - const networkId = options.networkId || 1; // Default to mainnet - const enabledChains = options.chains || ["cardano", "spark"]; // Default chains + const networkId = options.networkId || 0; // Default to testnet + const enabledChains = options.chains || ["cardano", "spark"]; const chains: MultiChainWalletInfo['chains'] = {}; // Generate single mnemonic for all chains (matches user-controlled pattern) @@ -91,7 +91,6 @@ export class WalletDeveloperControlled { data: sharedMnemonic.join(" "), }); - // Generate addresses for enabled chains using shared mnemonic and network if (enabledChains.includes("cardano")) { const tempWallet = new MeshWallet({ networkId: networkId, @@ -101,13 +100,12 @@ export class WalletDeveloperControlled { }); await tempWallet.init(); - const addresses = await tempWallet.getAddresses(); + const addresses = tempWallet.getAddresses(); const { pubKeyHash, stakeCredentialHash } = deserializeBech32Address(addresses.baseAddressBech32!); chains.cardano = { pubKeyHash, stakeCredentialHash, - address: addresses.baseAddressBech32!, }; } @@ -122,24 +120,22 @@ export class WalletDeveloperControlled { const publicKey = await tempSparkWallet.getIdentityPublicKey(); chains.spark = { - sparkAddress, publicKey, }; } - // Store unified wallet in database const walletData = { id: walletId, projectId: this.sdk.projectId, tags: options.tags || [], - key: encryptedKey, // Single shared mnemonic - networkId: networkId, // Single network for all chains + key: encryptedKey, + networkId: networkId, chains, createdAt: new Date().toISOString(), }; const { data, status } = await this.sdk.axiosInstance.post( - `api/project-wallet/multi-chain`, + `api/project-wallet`, walletData ); diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index d215d34..f1b558d 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -16,6 +16,8 @@ import { SparkTokenBalanceResult, SparkFreezeResult, SparkFrozenAddressesResult, + SparkTokenMetadata, + SparkTokenMetadataResponse, PaginationParams, } from "../../types/spark/dev-wallet"; @@ -484,25 +486,41 @@ export class SparkWalletDeveloperControlled { * Get metadata for a specific token. * * @param params - Token metadata query parameters - * @param params.tokenId - The token identifier to get metadata for - * @returns Promise that resolves to token metadata + * @param params.tokenIds - Array of token identifiers (up to 100 tokens) + * @returns Promise that resolves to token metadata response * * @example * ```typescript - * const metadata = await sparkWallet.getTokenMetadata({ tokenId: "spark1token123..." }); + * // Single token + * const response = await sparkWallet.getTokenMetadata({ tokenIds: ["btkn1token123..."] }); + * const metadata = response.metadata[0]; * console.log(`${metadata.name} (${metadata.ticker})`); + * + * // Multiple tokens + * const batchResponse = await sparkWallet.getTokenMetadata({ + * tokenIds: ["btkntoken1...", "btkn1token2...", "btkn1token3..."] + * }); + * batchResponse.metadata.forEach(token => console.log(token.name)); * ``` */ - async getTokenMetadata(params: { tokenId: string }): Promise { + async getTokenMetadata(params: { tokenIds: string[] }): Promise { + if (params.tokenIds.length === 0) { + return { metadata: [], total_count: 0 }; + } + + if (params.tokenIds.length > 100) { + throw new Error("Maximum 100 token IDs allowed per batch request"); + } + const { data, status } = await this.sdk.axiosInstance.post( - `api/spark/tokens/metadata`, + `api/spark/tokens/metadata/batch`, { - token_addresses: [params.tokenId] + token_addresses: params.tokenIds } ); if (status === 200) { - return data.metadata?.[0] || null; + return data as SparkTokenMetadataResponse; } throw new Error("Failed to get token metadata"); diff --git a/src/types/cardano/dev-wallet.ts b/src/types/cardano/dev-wallet.ts new file mode 100644 index 0000000..e4d9e20 --- /dev/null +++ b/src/types/cardano/dev-wallet.ts @@ -0,0 +1,76 @@ +/** + * Cardano-specific transaction result + */ +export interface CardanoTransactionResult { + transactionId: string; +} + +/** + * Parameters for querying Cardano token balance + */ +export interface CardanoTokenBalanceParams { + tokenId: string; + address: string; +} + +/** + * Result for Cardano token balance queries + */ +export interface CardanoTokenBalanceResult { + balance: string; +} + +/** + * Parameters for transferring Cardano tokens + */ +export interface CardanoTransferTokensParams { + tokenId: string; + amount: string; + toAddress: string; +} + +/** + * Individual recipient for batch Cardano operations + */ +export interface CardanoBatchRecipient { + address: string; + amount: string; +} + +/** + * Parameters for batch transferring Cardano tokens + */ +export interface CardanoBatchTransferParams { + tokenId: string; + recipients: CardanoBatchRecipient[]; +} + +/** + * Parameters for freezing Cardano tokens (CIP-113 compliance) + */ +export interface CardanoFreezeTokensParams { + address: string; + reason?: string; +} + +/** + * Parameters for unfreezing Cardano tokens (CIP-113 compliance) + */ +export interface CardanoUnfreezeTokensParams { + address: string; +} + +/** + * Result for Cardano freeze/unfreeze operations (CIP-113) + */ +export interface CardanoFreezeResult { + transactionId: string; + impactedTokens: string[]; +} + +/** + * Parameters for burning Cardano tokens + */ +export interface CardanoBurnTokensParams { + amount: string; +} \ No newline at end of file diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index be02b00..02d5238 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -28,10 +28,8 @@ export interface MultiChainWalletInfo { cardano?: { pubKeyHash: string; stakeCredentialHash: string; - address: string; }; spark?: { - sparkAddress: string; publicKey: string; }; }; diff --git a/src/types/spark/dev-wallet.ts b/src/types/spark/dev-wallet.ts index dd2aba5..880697b 100644 --- a/src/types/spark/dev-wallet.ts +++ b/src/types/spark/dev-wallet.ts @@ -123,4 +123,31 @@ export interface PaginationMeta { export interface SparkFrozenAddressesResult { frozenAddresses: SparkFrozenAddressInfo[]; pagination: PaginationMeta; +} + +/** + * Token metadata from Sparkscan API + */ +export interface SparkTokenMetadata { + tokenIdentifier: string; + tokenAddress: string; + name: string; + ticker: string; + decimals: number; + issuerPublicKey: string; + iconUrl: string; + holderCount: number; + priceUsd: number; + maxSupply: number | null; + isFreezable: boolean | null; + createdAt: string | null; + updatedAt: string | null; +} + +/** + * Response for token metadata batch query + */ +export interface SparkTokenMetadataResponse { + metadata: SparkTokenMetadata[]; + total_count: number; } \ No newline at end of file From 3f982a94586f07163d6faff4e6927ecffc33f71a Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Tue, 18 Nov 2025 03:09:15 +0800 Subject: [PATCH 08/30] refactor: rename wallet handlers for clarity in WalletDeveloperControlled class --- src/sdk/wallet-developer-controlled/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 1781e0c..0b3caa2 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -38,13 +38,13 @@ export class WalletDeveloperControlled { readonly sdk: Web3Sdk; // Chain-specific handlers (public access for direct operations) - readonly cardanoWallet: CardanoWalletDeveloperControlled; - readonly sparkWallet: SparkWalletDeveloperControlled; + readonly cardano: CardanoWalletDeveloperControlled; + readonly spark: SparkWalletDeveloperControlled; constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; - this.cardanoWallet = new CardanoWalletDeveloperControlled({ sdk }); - this.sparkWallet = new SparkWalletDeveloperControlled({ sdk }); + this.cardano = new CardanoWalletDeveloperControlled({ sdk }); + this.spark = new SparkWalletDeveloperControlled({ sdk }); } /** From 71ad21c032ff860f45ef4dcfdb25ce8793e2e281 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Tue, 18 Nov 2025 19:22:11 +0800 Subject: [PATCH 09/30] refactor: simplify wallet retrieval by removing walletId parameter from methods --- src/index.ts | 1 + src/sdk/wallet-developer-controlled/spark.ts | 43 ++++++++------------ 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5542709..0194af4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,5 +9,6 @@ export * from "./wallet-user-controlled"; export { isValidSparkAddress, type Bech32mTokenIdentifier, + encodeBech32mTokenIdentifier, SparkWallet, } from "@buildonspark/spark-sdk"; diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index f1b558d..5df3b6d 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -139,7 +139,6 @@ export class SparkWalletDeveloperControlled { /** * Retrieves a specific Spark wallet by ID and creates a wallet instance. * - * @param walletId - The unique identifier of the wallet to retrieve * @param decryptKey - Whether to decrypt and return the mnemonic key (default: false) * @returns Promise that resolves to wallet info and initialized IssuerSparkWallet instance * @@ -152,7 +151,6 @@ export class SparkWalletDeveloperControlled { * ``` */ async getWallet( - walletId: string, decryptKey = false, ): Promise<{ info: Web3ProjectSparkWallet; @@ -163,7 +161,7 @@ export class SparkWalletDeveloperControlled { } const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/spark/${walletId}`, + `api/project-wallet/${this.sdk.projectId}/spark`, ); if (status === 200) { @@ -211,7 +209,6 @@ export class SparkWalletDeveloperControlled { /** * Get token metadata for tokens created by a specific issuer wallet. * - * @param walletId - The ID of the issuer wallet to get token metadata for * @returns Promise that resolves to token metadata information * * @throws {Error} When token metadata retrieval fails @@ -222,15 +219,14 @@ export class SparkWalletDeveloperControlled { * console.log(`Token: ${metadata.tokenName} (${metadata.tokenSymbol})`); * ``` */ - async getIssuerTokenMetadata(walletId: string): Promise { - const { wallet } = await this.getWallet(walletId); + async getIssuerTokenMetadata(): Promise { + const { wallet } = await this.getWallet(); return await wallet.getIssuerTokenMetadata(); } /** * Creates a new token using a specific issuer wallet. * - * @param walletId - The ID of the issuer wallet to use for token creation * @param params - Token creation parameters * @param params.tokenName - The full name of the token * @param params.tokenTicker - The ticker symbol for the token @@ -281,7 +277,6 @@ export class SparkWalletDeveloperControlled { * 1. Mints tokens to the issuer wallet * 2. Transfers the minted tokens to the specified address * - * @param walletId - The ID of the issuer wallet to mint tokens with * @param params - Minting parameters * @param params.tokenizationId - The Bech32m token identifier to mint * @param params.amount - The amount of tokens to mint (as string to handle large numbers) @@ -344,7 +339,6 @@ export class SparkWalletDeveloperControlled { * 2. Performing a single mint operation to the issuer wallet * 3. Executing a single batch transfer to all recipients simultaneously * - * @param walletId - The ID of the issuer wallet to mint tokens with * @param params - Batch minting parameters * @param params.tokenizationId - The Bech32m token identifier to mint * @param params.recipients - Array of recipient addresses and amounts @@ -367,10 +361,9 @@ export class SparkWalletDeveloperControlled { * ``` */ async batchMintTokens( - walletId: string, params: SparkBatchMintParams ): Promise { - const { wallet } = await this.getWallet(walletId); + const { wallet } = await this.getWallet(); // Calculate total amount needed for all recipients const totalAmount = params.recipients.reduce( @@ -396,7 +389,6 @@ export class SparkWalletDeveloperControlled { /** * Transfers tokens from an issuer wallet to another Spark address. * - * @param walletId - The ID of the issuer wallet to transfer tokens from * @param params - Transfer parameters * @param params.tokenIdentifier - The Bech32m token identifier for the token to transfer * @param params.amount - The amount of tokens to transfer (as string to handle large numbers) @@ -416,10 +408,9 @@ export class SparkWalletDeveloperControlled { * ``` */ async transferTokens( - walletId: string, params: SparkTransferTokensParams ): Promise { - const { wallet } = await this.getWallet(walletId); + const { wallet } = await this.getWallet(); const result = await wallet.transferTokens({ tokenIdentifier: params.tokenIdentifier, @@ -434,21 +425,25 @@ export class SparkWalletDeveloperControlled { /** * Retrieves metadata for tokens created by a specific issuer wallet. - * - * @param walletId - The ID of the issuer wallet to get token metadata for + *x * @returns Promise that resolves to token metadata information * * @throws {Error} When token metadata retrieval fails * * @example * ```typescript - * const metadata = await sparkWallet.getCreatedTokens("wallet-id"); + * const metadata = await sparkWallet.getCreatedTokens(); * console.log(`Token: ${metadata.tokenName} (${metadata.tokenSymbol})`); * ``` */ - async getCreatedTokens(walletId: string): Promise { - const { wallet } = await this.getWallet(walletId); - return await wallet.getIssuerTokenMetadata(); + async getCreatedTokens(): Promise { + const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } + + return await sparkWallet.getIssuerTokenMetadata(); } /** @@ -563,7 +558,6 @@ export class SparkWalletDeveloperControlled { * This operation can only be performed by issuer wallets on freezable tokens. * Frozen tokens cannot be transferred until unfrozen by the issuer. * - * @param walletId - The ID of the issuer wallet with freeze authority * @param params - Freeze parameters * @param params.address - The Spark address to freeze tokens at * @returns Promise that resolves to freeze operation details @@ -579,10 +573,9 @@ export class SparkWalletDeveloperControlled { * ``` */ async freezeTokens( - walletId: string, params: SparkFreezeTokensParams ): Promise { - const { wallet } = await this.getWallet(walletId); + const { wallet } = await this.getWallet(); const result = await wallet.freezeTokens(params.address); @@ -598,7 +591,6 @@ export class SparkWalletDeveloperControlled { * This operation can only be performed by issuer wallets that previously froze the tokens. * Unfrozen tokens can be transferred normally again. * - * @param walletId - The ID of the issuer wallet with unfreeze authority * @param params - Unfreeze parameters * @param params.address - The Spark address to unfreeze tokens at * @returns Promise that resolves to unfreeze operation details @@ -614,10 +606,9 @@ export class SparkWalletDeveloperControlled { * ``` */ async unfreezeTokens( - walletId: string, params: SparkUnfreezeTokensParams ): Promise { - const { wallet } = await this.getWallet(walletId); + const { wallet } = await this.getWallet(); const result = await wallet.unfreezeTokens(params.address); From 1866ce133e979daecb95e688a17c66d232f2a240 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Sat, 22 Nov 2025 05:17:14 +0800 Subject: [PATCH 10/30] feat: spark dev controlled wallet update sdk --- src/index.ts | 11 + .../wallet-developer-controlled/cardano.ts | 2 +- src/sdk/wallet-developer-controlled/index.ts | 76 +++-- src/sdk/wallet-developer-controlled/spark.ts | 290 +++++++++++++----- src/spark/web3-spark-wallet.ts | 26 +- src/types/cardano/index.ts | 1 + src/types/core/index.ts | 1 - src/types/core/multi-chain.ts | 5 +- src/types/index.ts | 1 + src/types/spark/dev-wallet.ts | 4 +- src/types/spark/index.ts | 3 + 11 files changed, 293 insertions(+), 127 deletions(-) create mode 100644 src/types/cardano/index.ts diff --git a/src/index.ts b/src/index.ts index 0194af4..4b36328 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,18 @@ export * from "./wallet-user-controlled"; // Re-export Spark utilities to avoid installing full SDK in apps export { isValidSparkAddress, + decodeSparkAddress, + getNetworkFromSparkAddress, type Bech32mTokenIdentifier, encodeBech32mTokenIdentifier, SparkWallet, } from "@buildonspark/spark-sdk"; + +// Re-export our own Spark utilities +export { + extractIdentityPublicKey, + getSparkAddressFromPubkey, + convertLegacyToNewFormat, +} from "./chains/spark/utils"; + +export { type IssuerTokenMetadata } from "@buildonspark/issuer-sdk"; diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index 040fedf..fa20ae5 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -113,7 +113,7 @@ export class CardanoWalletDeveloperControlled { } const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/cardano/${walletId}`, + `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=cardano`, ); if (status === 200) { diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 0b3caa2..5480817 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -3,7 +3,8 @@ import { MultiChainWalletOptions, MultiChainWalletInfo, MultiChainWalletInstance, - SupportedChain + SupportedChain, + NetworkId } from "../../types/core/multi-chain"; import { CardanoWalletDeveloperControlled } from "./cardano"; import { SparkWalletDeveloperControlled } from "./spark"; @@ -68,12 +69,6 @@ export class WalletDeveloperControlled { * ``` */ async createWallet(options: MultiChainWalletOptions = {}): Promise { - // Check if project already has a wallet - const existingWallet = await this.getProjectWallet().catch(() => null); - if (existingWallet) { - throw new Error("Project already has a wallet. Use getWallet() to retrieve it or add chains to existing wallet."); - } - const project = await this.sdk.getProject(); if (!project.publicKey) { throw new Error("Project public key not found"); @@ -84,7 +79,7 @@ export class WalletDeveloperControlled { const enabledChains = options.chains || ["cardano", "spark"]; const chains: MultiChainWalletInfo['chains'] = {}; - // Generate single mnemonic for all chains (matches user-controlled pattern) + // Generate single mnemonic for all chains const sharedMnemonic = MeshWallet.brew() as string[]; const encryptedKey = await encryptWithPublicKey({ publicKey: project.publicKey, @@ -110,17 +105,29 @@ export class WalletDeveloperControlled { } if (enabledChains.includes("spark")) { - const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; - const { wallet: tempSparkWallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: sharedMnemonic.join(" "), - options: { network: sparkNetwork }, - }); - - const sparkAddress = await tempSparkWallet.getSparkAddress(); - const publicKey = await tempSparkWallet.getIdentityPublicKey(); + const [mainnetWallet, regtestWallet] = await Promise.all([ + IssuerSparkWallet.initialize({ + mnemonicOrSeed: sharedMnemonic.join(" "), + options: { network: "MAINNET" }, + }), + IssuerSparkWallet.initialize({ + mnemonicOrSeed: sharedMnemonic.join(" "), + options: { network: "REGTEST" }, + }) + ]); + + const [mainnetAddress, mainnetPublicKey, regtestAddress, regtestPublicKey] = await Promise.all([ + mainnetWallet.wallet.getSparkAddress(), + mainnetWallet.wallet.getIdentityPublicKey(), + regtestWallet.wallet.getSparkAddress(), + regtestWallet.wallet.getIdentityPublicKey() + ]); chains.spark = { - publicKey, + mainnetPublicKey, + mainnetAddress, + regtestPublicKey, + regtestAddress, }; } @@ -158,16 +165,18 @@ export class WalletDeveloperControlled { * @example * ```typescript * // Load specific chain - * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); + * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", 0, "spark"); * * // Load all available chains - * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id"); + * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id", 0, "cardano"); * ``` */ async getWallet( - chain?: SupportedChain, + projectWalletId: string, + networkId: NetworkId, + chain: SupportedChain, ): Promise { - const walletInfo = await this.getProjectWallet(); + const walletInfo = await this.getProjectWallet(projectWalletId); const instance: MultiChainWalletInstance = { info: walletInfo @@ -181,10 +190,9 @@ export class WalletDeveloperControlled { }); } - // Load requested chain or ALL available chains if no specific chain requested if ((chain === "cardano" || !chain) && walletInfo.chains.cardano && sharedMnemonic) { const cardanoWallet = new MeshWallet({ - networkId: walletInfo.networkId, + networkId: networkId, key: { type: "mnemonic", words: sharedMnemonic.split(" ") }, fetcher: this.sdk.providerFetcher, submitter: this.sdk.providerSubmitter, @@ -210,10 +218,12 @@ export class WalletDeveloperControlled { /** * Get the project's single multi-chain wallet */ - async getProjectWallet(): Promise { - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}` - ); + async getProjectWallet(walletId?: string): Promise { + const endpoint = walletId + ? `api/project-wallet/${this.sdk.projectId}/${walletId}` + : `api/project-wallet/${this.sdk.projectId}`; + + const { data, status } = await this.sdk.axiosInstance.get(endpoint); if (status === 200) { return data as MultiChainWalletInfo; @@ -221,16 +231,4 @@ export class WalletDeveloperControlled { throw new Error("Project wallet not found"); } - - /** - * Helper method to check if wallet has the supported chain - */ - private async hasWallet(walletId: string, chain: SupportedChain): Promise { - try { - const wallet = await this.getProjectWallet(); - return !!wallet.chains[chain]; - } catch { - return false; - } - } } diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index 5df3b6d..fb32f04 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -1,3 +1,5 @@ +// TODO: Add network parameters to all methods and clean up API calls to consistently use network params + import { Web3Sdk } from ".."; import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; import { Web3ProjectSparkWallet, TokenCreationParams } from "../../types"; @@ -8,7 +10,6 @@ import { SparkMintTokensParams, SparkBatchMintParams, SparkTransferTokensParams, - SparkTokenBalanceParams, SparkFreezeTokensParams, SparkUnfreezeTokensParams, SparkTransactionResult, @@ -16,10 +17,11 @@ import { SparkTokenBalanceResult, SparkFreezeResult, SparkFrozenAddressesResult, - SparkTokenMetadata, SparkTokenMetadataResponse, PaginationParams, } from "../../types/spark/dev-wallet"; +import { QueryTokenTransactionsResponse } from "@buildonspark/spark-sdk/dist/proto/spark_token"; +import { extractIdentityPublicKey } from "../../chains/spark/utils"; /** * SparkWalletDeveloperControlled - Manages Spark-specific developer-controlled wallets @@ -88,7 +90,6 @@ export class SparkWalletDeveloperControlled { options: { network }, }); - const sparkAddress = await sparkWallet.getSparkAddress(); const publicKey = await sparkWallet.getIdentityPublicKey(); const web3Wallet: Web3ProjectSparkWallet = { @@ -96,7 +97,6 @@ export class SparkWalletDeveloperControlled { key: encryptedMnemonic, tags: tags || [], projectId: this.sdk.projectId, - sparkAddress, publicKey, network, }; @@ -151,6 +151,8 @@ export class SparkWalletDeveloperControlled { * ``` */ async getWallet( + walletId: string, + network: "MAINNET" | "REGTEST" = "REGTEST", decryptKey = false, ): Promise<{ info: Web3ProjectSparkWallet; @@ -161,27 +163,27 @@ export class SparkWalletDeveloperControlled { } const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/spark`, + `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${network.toLowerCase()}`, ); if (status === 200) { - const web3Wallet = data as Web3ProjectSparkWallet; - + const sparkWallet = data as Web3ProjectSparkWallet; + const mnemonic = await decryptWithPrivateKey({ privateKey: this.sdk.privateKey, - encryptedDataJSON: web3Wallet.key, + encryptedDataJSON: sparkWallet.key, }); if (decryptKey) { - web3Wallet.key = mnemonic; + sparkWallet.key = mnemonic; } - - const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ + + const { wallet: issuerSparkWallet } = await IssuerSparkWallet.initialize({ mnemonicOrSeed: mnemonic, - options: { network: web3Wallet.network }, + options: { network: sparkWallet.network }, }); - return { info: web3Wallet, wallet: sparkWallet }; + return { info: sparkWallet, wallet: issuerSparkWallet }; } throw new Error("Failed to get Spark wallet"); @@ -215,15 +217,20 @@ export class SparkWalletDeveloperControlled { * * @example * ```typescript - * const metadata = await sparkWallet.getIssuerTokenMetadata("wallet-id"); - * console.log(`Token: ${metadata.tokenName} (${metadata.tokenSymbol})`); + * const metadata = await sparkWallet.getIssuerTokenMetadata("wallet-123"); + * console.log(`Token: ${metadata.tokenName} (${metadata.tokenTicker})`); * ``` */ - async getIssuerTokenMetadata(): Promise { - const { wallet } = await this.getWallet(); - return await wallet.getIssuerTokenMetadata(); + async getIssuerTokenMetadata(walletId: string): Promise { + const { wallet: sparkWallet } = await this.getWallet(walletId); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } + return await sparkWallet.getIssuerTokenMetadata(); } + /** * Creates a new token using a specific issuer wallet. * @@ -239,18 +246,19 @@ export class SparkWalletDeveloperControlled { * * @example * ```typescript - * const result = await sparkWallet.createToken("wallet-id", { + * const result = await sparkWallet.createToken({ * tokenName: "My Token", * tokenTicker: "MTK", * decimals: 8, * maxSupply: "1000000", - * isFreezable: true + * isFreezable: true, + * walletId: "wallet-123" * }); * console.log(`Token created with transaction: ${result.transactionId}`); * ``` */ - async createToken(params: TokenCreationParams): Promise { - const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); + async createToken(params: TokenCreationParams & { walletId: string }): Promise { + const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId); if (!sparkWallet) { throw new Error("Spark wallet not available for this project"); @@ -264,6 +272,21 @@ export class SparkWalletDeveloperControlled { isFreezable: params.isFreezable, }); + try { + const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); + const rawTokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); + + await this.sdk.axiosInstance.post('/api/tokenization/tokens', { + tokenId: rawTokenIdHex, + projectId: this.sdk.projectId, + walletId: walletInfo.id, + chain: "spark", + network: walletInfo.network.toLowerCase() + }); + } catch (saveError) { + console.warn("Failed to save token to main app:", saveError); + } + return { transactionId, }; @@ -289,20 +312,22 @@ export class SparkWalletDeveloperControlled { * ```typescript * // Mint to issuer wallet * const result1 = await sparkWallet.mintTokens({ - * tokenization_id: "spark1token123...", - * amount: "1000000" + * tokenizationId: "btkn1token123...", + * amount: "1000000", + * walletId: "wallet-123" * }); * * // Mint and transfer to specific address (two-step process) * const result2 = await sparkWallet.mintTokens({ - * tokenization_id: "spark1token123...", + * tokenizationId: "btkn1token123...", * amount: "1000000", - * address: "spark1recipient456..." + * address: "spark1recipient456...", + * walletId: "wallet-123" * }); * ``` */ - async mintTokens(params: SparkMintTokensParams): Promise { - const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); + async mintTokens(params: SparkMintTokensParams & { walletId: string }): Promise { + const { wallet: sparkWallet } = await this.getWallet(params.walletId); if (!sparkWallet) { throw new Error("Spark wallet not available for this project"); @@ -361,9 +386,13 @@ export class SparkWalletDeveloperControlled { * ``` */ async batchMintTokens( - params: SparkBatchMintParams + params: SparkBatchMintParams & { walletId: string } ): Promise { - const { wallet } = await this.getWallet(); + const { wallet: sparkWallet } = await this.getWallet(params.walletId); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } // Calculate total amount needed for all recipients const totalAmount = params.recipients.reduce( @@ -371,14 +400,14 @@ export class SparkWalletDeveloperControlled { 0n ); - const mintTransactionId = await wallet.mintTokens(totalAmount); + const mintTransactionId = await sparkWallet.mintTokens(totalAmount); const receiverOutputs = params.recipients.map(recipient => ({ tokenIdentifier: params.tokenizationId, tokenAmount: BigInt(recipient.amount), receiverSparkAddress: recipient.address, })); - const batchTransferTransactionId = await wallet.batchTransferTokens(receiverOutputs); + const batchTransferTransactionId = await sparkWallet.batchTransferTokens(receiverOutputs); return { mintTransactionId, @@ -399,20 +428,25 @@ export class SparkWalletDeveloperControlled { * * @example * ```typescript - * const result = await sparkWallet.transferTokens("wallet-id", { - * tokenIdentifier: "spark1abc...", + * const result = await sparkWallet.transferTokens({ + * tokenIdentifier: "btkn1abc...", * amount: "100000", - * toAddress: "spark1def..." + * toAddress: "spark1def...", + * walletId: "wallet-123" * }); * console.log(`Transferred tokens with transaction: ${result.transactionId}`); * ``` */ async transferTokens( - params: SparkTransferTokensParams + params: SparkTransferTokensParams & { walletId: string } ): Promise { - const { wallet } = await this.getWallet(); + const { wallet: sparkWallet } = await this.getWallet(params.walletId); - const result = await wallet.transferTokens({ + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } + + const result = await sparkWallet.transferTokens({ tokenIdentifier: params.tokenIdentifier, tokenAmount: BigInt(params.amount), receiverSparkAddress: params.toAddress, @@ -423,29 +457,6 @@ export class SparkWalletDeveloperControlled { }; } - /** - * Retrieves metadata for tokens created by a specific issuer wallet. - *x - * @returns Promise that resolves to token metadata information - * - * @throws {Error} When token metadata retrieval fails - * - * @example - * ```typescript - * const metadata = await sparkWallet.getCreatedTokens(); - * console.log(`Token: ${metadata.tokenName} (${metadata.tokenSymbol})`); - * ``` - */ - async getCreatedTokens(): Promise { - const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - - return await sparkWallet.getIssuerTokenMetadata(); - } - /** * Retrieves token balance for a specific address using Sparkscan API. * @@ -465,19 +476,68 @@ export class SparkWalletDeveloperControlled { * console.log(`Balance: ${balance.balance} tokens`); * ``` */ - async getTokenBalance(params: SparkTokenBalanceParams): Promise { - const { data, status } = await this.sdk.axiosInstance.get( - `api/spark/tokens/${params.tokenId}/balance?address=${params.address}` - ); + async getTokenBalance(walletId: string): Promise { + try { + const { wallet: sparkWallet } = await this.getWallet(walletId); - if (status === 200) { - return data; + if (!sparkWallet) { + throw new Error("Spark wallet not found"); + } + + const balanceResult = await sparkWallet.getIssuerTokenBalance(); + + return { balance: balanceResult.balance.toString() }; + } catch (error) { + throw new Error(`Failed to get token balance: ${error}`); } + } - throw new Error("Failed to get token balance"); + /** + * Query token transactions using Sparkscan API + * @param params - Query parameters for filtering transactions + * @param params.tokenIdentifiers - Array of token identifiers to filter by + * @param params.ownerPublicKeys - Optional array of owner public keys to filter by + * @param params.issuerPublicKeys - Optional array of issuer public keys to filter by + * @param params.tokenTransactionHashes - Optional array of transaction hashes to filter by + * @param params.outputIds - Optional array of output IDs to filter by + * @returns Promise resolving to array of token transactions + * + * @example + * ```typescript + * const transactions = await sparkWallet.queryTokenTransactions({ + * tokenIdentifiers: ["btkn1..."], + * ownerPublicKeys: ["spark1..."] + * }); + * console.log("Token transactions:", transactions); + * ``` + */ + async queryTokenTransactions(params: { + walletId: string; + sparkAddresses?: string[]; + ownerPublicKeys?: string[]; + issuerPublicKeys?: string[]; + tokenTransactionHashes?: string[]; + tokenIdentifiers?: string[]; + outputIds?: string[]; + order?: "asc" | "desc"; + pageSize?: number; + offset?: number; + }): Promise { + try { + const { wallet: sparkWallet } = await this.getWallet(params.walletId); + + if (!sparkWallet) { + throw new Error("Spark wallet not found"); + } + + return await sparkWallet.queryTokenTransactions(params); + } catch (error) { + throw new Error(`Failed to query token transactions: ${error}`); + } } /** + * TODO: Remove this, we should use the issuer sdk * Get metadata for a specific token. * * @param params - Token metadata query parameters @@ -538,8 +598,8 @@ export class SparkWalletDeveloperControlled { * console.log(`Burned tokens with transaction: ${result.transactionId}`); * ``` */ - async burnTokens(params: { amount: string }): Promise { - const { sparkWallet } = await this.sdk.wallet.getWallet("spark"); + async burnTokens(params: { amount: string; walletId: string }): Promise { + const { wallet: sparkWallet } = await this.getWallet(params.walletId); if (!sparkWallet) { throw new Error("Spark wallet not available for this project"); @@ -566,18 +626,48 @@ export class SparkWalletDeveloperControlled { * * @example * ```typescript - * const result = await sparkWallet.freezeTokens("issuer-wallet-id", { - * address: "spark1suspicious123..." + * const result = await sparkWallet.freezeTokens({ + * address: "spark1suspicious123...", + * walletId: "wallet-123" * }); * console.log(`Frozen ${result.impactedTokenAmount} tokens at ${result.impactedOutputIds.length} outputs`); * ``` */ async freezeTokens( - params: SparkFreezeTokensParams + params: SparkFreezeTokensParams & { walletId: string } ): Promise { - const { wallet } = await this.getWallet(); + const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } - const result = await wallet.freezeTokens(params.address); + const result = await sparkWallet.freezeTokens(params.address); + + try { + const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); + + // Extract public key hash from Spark address + const publicKeyHash = extractIdentityPublicKey(params.address); + if (!publicKeyHash) { + throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); + } + + await this.sdk.axiosInstance.post('/api/tokenization/frozen-addresses', { + tokenId, + projectId: this.sdk.projectId, + projectWalletId: walletInfo.id, + chain: "spark", + network: walletInfo.network.toLowerCase(), + publicKeyHash: publicKeyHash, + isFrozen: true, + freezeReason: params.freezeReason || "Frozen by issuer", + frozenAt: new Date().toISOString() + }); + } catch (saveError) { + console.warn("Failed to save freeze operation to main app:", saveError); + } return { impactedOutputIds: result.impactedOutputIds, @@ -599,18 +689,42 @@ export class SparkWalletDeveloperControlled { * * @example * ```typescript - * const result = await sparkWallet.unfreezeTokens("issuer-wallet-id", { - * address: "spark1cleared123..." + * const result = await sparkWallet.unfreezeTokens({ + * address: "spark1cleared123...", + * walletId: "wallet-123" * }); * console.log(`Unfrozen ${result.impactedTokenAmount} tokens at ${result.impactedOutputIds.length} outputs`); * ``` */ async unfreezeTokens( - params: SparkUnfreezeTokensParams + params: SparkUnfreezeTokensParams & { walletId: string } ): Promise { - const { wallet } = await this.getWallet(); + const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId); + + if (!sparkWallet) { + throw new Error("Spark wallet not available for this project"); + } - const result = await wallet.unfreezeTokens(params.address); + const result = await sparkWallet.unfreezeTokens(params.address); + + try { + const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); + + const publicKeyHash = extractIdentityPublicKey(params.address); + if (!publicKeyHash) { + throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); + } + + await this.sdk.axiosInstance.put('/api/tokenization/frozen-addresses', { + tokenId, + publicKeyHash: publicKeyHash, + projectId: this.sdk.projectId, + projectWalletId: walletInfo.id, + }); + } catch (saveError) { + console.warn("Failed to save unfreeze operation to main app:", saveError); + } return { impactedOutputIds: result.impactedOutputIds, @@ -647,16 +761,28 @@ export class SparkWalletDeveloperControlled { * console.log(`Page ${frozenInfo.pagination.currentPage} of ${frozenInfo.pagination.totalPages}`); * ``` */ - async getFrozenAddresses(params?: PaginationParams): Promise { + async getFrozenAddresses(params?: PaginationParams & { walletId: string }): Promise { + if (!params?.walletId) { + throw new Error("walletId is required for getFrozenAddresses"); + } + + const targetNetwork = params.network || "REGTEST"; + const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId, targetNetwork); + const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); + const queryParams = new URLSearchParams({ + tokenId: tokenId, projectId: this.sdk.projectId, + chain: "spark", + network: walletInfo.network.toLowerCase(), ...(params?.page && { page: params.page.toString() }), ...(params?.limit && { limit: params.limit.toString() }), ...(params?.offset && { offset: params.offset.toString() }), }); const { data, status } = await this.sdk.axiosInstance.get( - `api/spark/frozen-addresses?${queryParams.toString()}` + `api/tokenization/frozen-addresses?${queryParams.toString()}` ); if (status === 200) { diff --git a/src/spark/web3-spark-wallet.ts b/src/spark/web3-spark-wallet.ts index 2b78011..b719a72 100644 --- a/src/spark/web3-spark-wallet.ts +++ b/src/spark/web3-spark-wallet.ts @@ -140,7 +140,7 @@ export class Web3SparkWallet { } /** - * Create a new token (basic implementation - will be enhanced by SparkTokenIssuer) + * Create a new token */ async createToken(params: Spark.TokenCreationParams): Promise { if (!this.projectId || !this.appUrl) { @@ -161,7 +161,7 @@ export class Web3SparkWallet { tokenName: params.tokenName, tokenTicker: params.tokenTicker, decimals: String(params.decimals), - maxSupply: params.maxSupply.toString(), + maxSupply: params.maxSupply?.toString() ?? "0", isFreezable: params.isFreezable ? "true" : "false", }, this.appUrl, @@ -548,6 +548,28 @@ export class Web3SparkWallet { } } + /** + * Get the balance of a specific token for the current wallet address + * @param tokenIdentifier - The token identifier to check balance for + * @param address - Optional specific address to check (defaults to wallet address) + * @returns Promise resolving to the token balance as a string, or "0" if not found + */ + async getTokenBalance(tokenIdentifier: string, address?: string): Promise { + try { + const tokensResponse = await this.getAddressTokens(address); + + const token = tokensResponse.tokens?.find( + (token) => token.tokenIdentifier === tokenIdentifier || token.tokenAddress === tokenIdentifier + ); + + return token ? token.balance.toString() : "0"; + } catch (error) { + throw new ApiError({ + code: 5, + info: `Failed to get token balance: ${error}`, + }); + } + } /** * Query token transactions using Sparkscan API diff --git a/src/types/cardano/index.ts b/src/types/cardano/index.ts new file mode 100644 index 0000000..5e078c9 --- /dev/null +++ b/src/types/cardano/index.ts @@ -0,0 +1 @@ +export * from "./dev-wallet"; \ No newline at end of file diff --git a/src/types/core/index.ts b/src/types/core/index.ts index d1eed9f..869ae86 100644 --- a/src/types/core/index.ts +++ b/src/types/core/index.ts @@ -41,7 +41,6 @@ export type Web3ProjectSparkWallet = { key: string; tags: string[]; projectId: string; - sparkAddress: string; publicKey: string; network: "MAINNET" | "REGTEST"; }; diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index 02d5238..cb1ac9d 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -30,7 +30,10 @@ export interface MultiChainWalletInfo { stakeCredentialHash: string; }; spark?: { - publicKey: string; + mainnetPublicKey: string; + mainnetAddress: string; + regtestPublicKey: string; + regtestAddress: string; }; }; createdAt: string; diff --git a/src/types/index.ts b/src/types/index.ts index fc71847..3b93ff7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,3 +2,4 @@ export * from "./core"; export * from "./user"; export * from "./window"; export * from "./spark"; +export * from "./cardano"; diff --git a/src/types/spark/dev-wallet.ts b/src/types/spark/dev-wallet.ts index 880697b..30618fe 100644 --- a/src/types/spark/dev-wallet.ts +++ b/src/types/spark/dev-wallet.ts @@ -39,7 +39,7 @@ export interface SparkTransferTokensParams { */ export interface SparkTokenBalanceParams { tokenId: string; - address: string; + address?: string; } /** @@ -69,6 +69,7 @@ export interface SparkTokenBalanceResult { */ export interface SparkFreezeTokensParams { address: string; + freezeReason?: string; } /** @@ -103,6 +104,7 @@ export interface PaginationParams { page?: number; limit?: number; offset?: number; + network?: "MAINNET" | "REGTEST"; } /** diff --git a/src/types/spark/index.ts b/src/types/spark/index.ts index 2132d4a..aa0cbb2 100644 --- a/src/types/spark/index.ts +++ b/src/types/spark/index.ts @@ -247,6 +247,9 @@ export interface MintTokenParams { recipientAddress: string; } +// Export dev-wallet types +export * from "./dev-wallet"; + // Types copied from @buildonspark/spark-sdk since they are currently private // Source: https://github.com/buildonspark/spark-sdk export enum TransferDirection { From ccfdb01909835256bd50820da6dd78909dfed8c6 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Tue, 25 Nov 2025 01:20:42 +0800 Subject: [PATCH 11/30] feat: dev controlled spark wallet update --- src/sdk/wallet-developer-controlled/spark.ts | 24 ++++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index fb32f04..37e3f5c 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -55,14 +55,23 @@ export class SparkWalletDeveloperControlled { * @param params - Wallet creation parameters * @param params.tags - Optional tags to organize the wallet * @param params.network - Network to create the wallet on (default: "REGTEST") - * @returns Promise that resolves to the created Spark wallet + * @returns Promise that resolves to both wallet info and ready-to-use IssuerSparkWallet instance * * @example * ```typescript - * const wallet = await sparkWallet.createWallet({ + * const { info, wallet } = await sparkWallet.createWallet({ * tags: ["tokenization", "mainnet"], * network: "MAINNET" * }); + * + * // Use wallet instance immediately (no privateKey needed) + * const tokenTx = await wallet.createToken({ + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * maxSupply: BigInt("1000000"), + * isFreezable: true + * }); * ``` */ async createWallet({ @@ -71,14 +80,16 @@ export class SparkWalletDeveloperControlled { }: { tags?: string[]; network?: "MAINNET" | "REGTEST"; - } = {}): Promise { + } = {}): Promise<{ + info: Web3ProjectSparkWallet; + wallet: IssuerSparkWallet; + }> { const project = await this.sdk.getProject(); if (!project.publicKey) { throw new Error("Project public key not found"); } - // Generate mnemonic for Spark wallet const mnemonic = EmbeddedWallet.brew(256); const encryptedMnemonic = await encryptWithPublicKey({ publicKey: project.publicKey, @@ -107,7 +118,10 @@ export class SparkWalletDeveloperControlled { ); if (status === 200) { - return data as Web3ProjectSparkWallet; + return { + info: data as Web3ProjectSparkWallet, + wallet: sparkWallet + }; } throw new Error("Failed to create Spark wallet"); From 049537cc757901479a4fd79ef226c1f869422ffe Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Wed, 26 Nov 2025 07:37:50 +0800 Subject: [PATCH 12/30] feat: add transaction logging for spark token operations and type improvements --- examples/developer-controlled-wallet/index.ts | 84 ++ src/sdk/index.ts | 1 + src/sdk/sponsorship/index.ts | 3 +- .../wallet-developer-controlled/cardano.ts | 70 +- src/sdk/wallet-developer-controlled/index.ts | 352 +++++-- src/sdk/wallet-developer-controlled/spark.ts | 959 ++++++------------ src/types/core/multi-chain.ts | 7 +- src/types/spark/dev-wallet.ts | 138 +-- 8 files changed, 670 insertions(+), 944 deletions(-) create mode 100644 examples/developer-controlled-wallet/index.ts diff --git a/examples/developer-controlled-wallet/index.ts b/examples/developer-controlled-wallet/index.ts new file mode 100644 index 0000000..948d2c2 --- /dev/null +++ b/examples/developer-controlled-wallet/index.ts @@ -0,0 +1,84 @@ +import { Web3Sdk } from "@meshsdk/web3-sdk"; + +/** + * Example: Developer-Controlled Wallet with New API + * + * This example demonstrates the new wallet-first API for developer-controlled wallets. + * Perfect for token issuance, treasury management, and automated operations. + */ + +async function main() { + // Initialize SDK + const sdk = new Web3Sdk({ + projectId: "your-project-id", + apiKey: "your-api-key", + network: "testnet", // or "mainnet" + appUrl: "https://your-app.com", + privateKey: "your-private-key", // Required for developer-controlled wallets + }); + + console.log("๐Ÿš€ Creating developer-controlled wallet..."); + + // Create wallet with both Spark and Cardano chains (shared mnemonic) + const { sparkWallet, cardanoWallet } = await sdk.wallet.createWallet({ + tags: ["tokenization", "treasury"] + }); + + console.log("โœ… Wallet created!"); + + // === SPARK TOKEN OPERATIONS === + + console.log("\n๐Ÿช™ Creating Spark token..."); + const tokenTxId = await sparkWallet.createToken({ + tokenName: "Example Token", + tokenTicker: "EXAM", + decimals: 8, + maxSupply: 1000000n, + isFreezable: true + }); + console.log("Token created:", tokenTxId); + + console.log("\n๐Ÿ’ฐ Minting tokens..."); + const mintTxId = await sparkWallet.mintTokens(BigInt("100000")); + console.log("Minted tokens:", mintTxId); + + console.log("\n๐Ÿ“Š Getting token info..."); + const balance = await sparkWallet.getTokenBalance(); + const metadata = await sparkWallet.getTokenMetadata(); + console.log("Token balance:", balance.balance); + console.log("Token name:", metadata.tokenName); + + console.log("\n๐Ÿ“ค Transferring tokens..."); + const transferTxId = await sparkWallet.transferTokens({ + tokenIdentifier: "your-token-identifier", + amount: BigInt("1000"), + toAddress: "spark1recipient..." + }); + console.log("Transfer complete:", transferTxId); + + // === COMPLIANCE OPERATIONS === + + console.log("\n๐Ÿšซ Freezing tokens for compliance..."); + const freezeResult = await sparkWallet.freezeTokens({ + address: "spark1suspicious...", + freezeReason: "Compliance investigation" + }); + console.log("Frozen outputs:", freezeResult.impactedOutputIds.length); + + console.log("\nโœ… Unfreezing tokens..."); + const unfreezeResult = await sparkWallet.unfreezeTokens({ + address: "spark1suspicious..." + }); + console.log("Unfrozen outputs:", unfreezeResult.impactedOutputIds.length); + + // === LOADING EXISTING WALLET === + + console.log("\n๐Ÿ”„ Loading existing wallet..."); + const { sparkWallet: existingWallet } = await sdk.wallet.initWallet("existing-wallet-id"); + + const existingBalance = await existingWallet.getTokenBalance(); + console.log("Existing wallet balance:", existingBalance.balance); +} + +// Run example +main().catch(console.error); \ No newline at end of file diff --git a/src/sdk/index.ts b/src/sdk/index.ts index e9dac87..119dd62 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -151,3 +151,4 @@ export class Web3Sdk { } export * from "./sponsorship"; +export * from "./wallet-developer-controlled"; diff --git a/src/sdk/sponsorship/index.ts b/src/sdk/sponsorship/index.ts index 014287c..c0ec8f8 100644 --- a/src/sdk/sponsorship/index.ts +++ b/src/sdk/sponsorship/index.ts @@ -340,9 +340,8 @@ export class Sponsorship { } private async getSponsorWallet(projectWalletId: string) { - const networkId: 0 | 1 = this.sdk.network === "mainnet" ? 1 : 0; // For sponsorship, we use direct Cardano wallet access - const walletResult = await this.sdk.wallet.cardano.getWallet(projectWalletId, networkId); + const walletResult = await this.sdk.wallet.cardano.getWallet(projectWalletId); return walletResult.wallet; } diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index fa20ae5..ff9998a 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -21,65 +21,17 @@ import { */ export class CardanoWalletDeveloperControlled { readonly sdk: Web3Sdk; - - constructor({ sdk }: { sdk: Web3Sdk }) { + private wallet: MeshWallet | null = null; + private walletInfo: Web3ProjectCardanoWallet | null = null; + + constructor({ sdk, wallet, walletInfo }: { + sdk: Web3Sdk; + wallet?: MeshWallet; + walletInfo?: Web3ProjectCardanoWallet; + }) { this.sdk = sdk; - } - - /** - * Creates a new Cardano wallet associated with the current project. - */ - async createWallet({ - tags, - }: { tags?: string[] } = {}): Promise { - const project = await this.sdk.getProject(); - - if (!project.publicKey) { - throw new Error("Project public key not found"); - } - - const mnemonic = MeshWallet.brew() as string[]; - const encryptedMnemonic = await encryptWithPublicKey({ - publicKey: project.publicKey, - data: mnemonic.join(" "), - }); - - const _wallet = new MeshWallet({ - networkId: 1, - key: { - type: "mnemonic", - words: mnemonic, - }, - fetcher: this.sdk.providerFetcher, - submitter: this.sdk.providerSubmitter, - }); - await _wallet.init(); - - const addresses = await _wallet.getAddresses(); - const baseAddressBech32 = addresses.baseAddressBech32!; - - const { pubKeyHash, stakeCredentialHash } = - deserializeBech32Address(baseAddressBech32); - - const web3Wallet: Web3ProjectCardanoWallet = { - id: uuidv4(), - key: encryptedMnemonic, - tags: tags || [], - projectId: this.sdk.projectId, - pubKeyHash: pubKeyHash, - stakeCredentialHash: stakeCredentialHash, - }; - - const { data, status } = await this.sdk.axiosInstance.post( - `api/project-wallet/cardano`, - web3Wallet, - ); - - if (status === 200) { - return data as Web3ProjectCardanoWallet; - } - - throw new Error("Failed to create Cardano wallet"); + this.wallet = wallet || null; + this.walletInfo = walletInfo || null; } /** @@ -102,7 +54,6 @@ export class CardanoWalletDeveloperControlled { */ async getWallet( walletId: string, - networkId: 0 | 1, decryptKey = false, ): Promise<{ info: Web3ProjectCardanoWallet; @@ -128,6 +79,7 @@ export class CardanoWalletDeveloperControlled { web3Wallet.key = mnemonic; } + const networkId = this.sdk.network === "mainnet" ? 1 : 0; const wallet = new MeshWallet({ networkId: networkId, key: { diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 5480817..616b0a1 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -1,11 +1,9 @@ import { Web3Sdk } from ".."; import { - MultiChainWalletOptions, MultiChainWalletInfo, MultiChainWalletInstance, - SupportedChain, - NetworkId -} from "../../types/core/multi-chain"; + SupportedChain} from "../../types/core/multi-chain"; +import { Web3ProjectCardanoWallet, Web3ProjectSparkWallet } from "../../types"; import { CardanoWalletDeveloperControlled } from "./cardano"; import { SparkWalletDeveloperControlled } from "./spark"; import { MeshWallet } from "@meshsdk/wallet"; @@ -18,29 +16,36 @@ export { SparkWalletDeveloperControlled } from "./spark"; /** * The `WalletDeveloperControlled` class provides functionality for managing developer-controlled wallets - * within a Web3 project. Supports multi-chain wallets with UTXO-native design. + * within a Web3 project. * * @example * ```typescript - * // Create multi-chain wallet - * const walletInfo = await sdk.wallet.createWallet({ - * tags: ["minting"], - * networks: { cardano: 1, spark: 1 } + * // โœ… Create wallet with both chains + * const { sparkWallet, cardanoWallet } = await sdk.wallet.createWallet({ + * tags: ["tokenization"], + * network: "MAINNET" * }); * - * // Get specific chain for performance - * const { sparkWallet } = await sdk.wallet.getWallet(walletInfo.id, 1, "spark"); + * // Use Spark wallet directly + * await sparkWallet.createToken({ + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * isFreezable: true + * }); + * + * // Load existing wallet + * const { sparkWallet: existingWallet } = await sdk.wallet.initWallet("wallet-id"); + * await existingWallet.mintTokens(BigInt("1000000")); * - * // Get all chains - * const { cardanoWallet, sparkWallet } = await sdk.wallet.getWallet(walletInfo.id, 1); + * // Or access via + * await sdk.wallet.spark.getWallet("wallet-id"); * ``` */ export class WalletDeveloperControlled { readonly sdk: Web3Sdk; - - // Chain-specific handlers (public access for direct operations) - readonly cardano: CardanoWalletDeveloperControlled; - readonly spark: SparkWalletDeveloperControlled; + cardano: CardanoWalletDeveloperControlled; + spark: SparkWalletDeveloperControlled; constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; @@ -49,131 +54,244 @@ export class WalletDeveloperControlled { } /** - * Creates a new multi-chain wallet associated with the current project. - * One wallet per project with unified ID containing all chain keys. + * Creates a new developer-controlled wallet with both Spark and Cardano chains using shared mnemonic. * - * @param options - Multi-chain wallet creation options - * @param options.tags - Optional tags to organize the wallet - * @param options.networks - Network configuration for each chain - * @returns Promise that resolves to multi-chain wallet information - * - * @throws {Error} When wallet creation fails or project has existing wallet + * @param options - Wallet creation options + * @param options.networkId - Network ID (0 = testnet, 1 = mainnet) + * @returns Promise that resolves to both chain wallet instances * * @example * ```typescript - * const wallet = await sdk.wallet.createWallet({ - * tags: ["tokenization", "mainnet"], - * networkId: 1, // Single network for all chains - * chains: ["cardano", "spark"] + * const { sparkWallet, cardanoWallet } = await sdk.wallet.createWallet({ + * tags: ["tokenization"], + * networkId: 1 // mainnet + * }); + * + * // Both wallets share the same mnemonic + * await sparkWallet.createToken({ + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * isFreezable: true * }); * ``` */ - async createWallet(options: MultiChainWalletOptions = {}): Promise { + async createWallet(options: { + tags?: string[]; + } = {}): Promise<{ + info: MultiChainWalletInfo; + sparkWallet: SparkWalletDeveloperControlled; + cardanoWallet: MeshWallet; + }> { const project = await this.sdk.getProject(); if (!project.publicKey) { throw new Error("Project public key not found"); } + const networkId = this.sdk.network === "mainnet" ? 1 : 0; const walletId = uuidv4(); - const networkId = options.networkId || 0; // Default to testnet - const enabledChains = options.chains || ["cardano", "spark"]; - const chains: MultiChainWalletInfo['chains'] = {}; - - // Generate single mnemonic for all chains - const sharedMnemonic = MeshWallet.brew() as string[]; + const mnemonic = MeshWallet.brew() as string[]; const encryptedKey = await encryptWithPublicKey({ publicKey: project.publicKey, - data: sharedMnemonic.join(" "), + data: mnemonic.join(" "), }); - if (enabledChains.includes("cardano")) { - const tempWallet = new MeshWallet({ - networkId: networkId, - key: { type: "mnemonic", words: sharedMnemonic }, - fetcher: this.sdk.providerFetcher, - submitter: this.sdk.providerSubmitter, - }); - await tempWallet.init(); + const cardanoWallet = new MeshWallet({ + networkId: networkId, + key: { type: "mnemonic", words: mnemonic }, + fetcher: this.sdk.providerFetcher, + submitter: this.sdk.providerSubmitter, + }); + await cardanoWallet.init(); - const addresses = tempWallet.getAddresses(); - const { pubKeyHash, stakeCredentialHash } = deserializeBech32Address(addresses.baseAddressBech32!); + const [{ wallet: sparkMainnetWallet }, { wallet: sparkRegtestWallet }] = await Promise.all([ + IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic.join(" "), + options: { network: "MAINNET" }, + }), + IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic.join(" "), + options: { network: "REGTEST" }, + }), + ]); - chains.cardano = { - pubKeyHash, - stakeCredentialHash, - }; - } + const addresses = cardanoWallet.getAddresses(); + const { pubKeyHash, stakeCredentialHash } = deserializeBech32Address(addresses.baseAddressBech32!); + const [mainnetPublicKey, regtestPublicKey] = await Promise.all([ + sparkMainnetWallet.getIdentityPublicKey(), + sparkRegtestWallet.getIdentityPublicKey(), + ]); - if (enabledChains.includes("spark")) { - const [mainnetWallet, regtestWallet] = await Promise.all([ - IssuerSparkWallet.initialize({ - mnemonicOrSeed: sharedMnemonic.join(" "), - options: { network: "MAINNET" }, - }), - IssuerSparkWallet.initialize({ - mnemonicOrSeed: sharedMnemonic.join(" "), - options: { network: "REGTEST" }, - }) - ]); - - const [mainnetAddress, mainnetPublicKey, regtestAddress, regtestPublicKey] = await Promise.all([ - mainnetWallet.wallet.getSparkAddress(), - mainnetWallet.wallet.getIdentityPublicKey(), - regtestWallet.wallet.getSparkAddress(), - regtestWallet.wallet.getIdentityPublicKey() - ]); - - chains.spark = { - mainnetPublicKey, - mainnetAddress, - regtestPublicKey, - regtestAddress, - }; - } + const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; + const sparkWallet = networkId === 1 ? sparkMainnetWallet : sparkRegtestWallet; const walletData = { id: walletId, projectId: this.sdk.projectId, tags: options.tags || [], key: encryptedKey, - networkId: networkId, - chains, + networkId, + chains: { + cardano: { pubKeyHash, stakeCredentialHash }, + spark: { mainnetPublicKey, regtestPublicKey } + }, createdAt: new Date().toISOString(), }; - const { data, status } = await this.sdk.axiosInstance.post( + const { status } = await this.sdk.axiosInstance.post( `api/project-wallet`, walletData ); if (status === 200) { - return data as MultiChainWalletInfo; + const sparkWalletInfo: Web3ProjectSparkWallet = { + id: walletId, + projectId: this.sdk.projectId, + tags: options.tags || [], + key: encryptedKey, + publicKey: networkId === 1 ? mainnetPublicKey : regtestPublicKey, + network: sparkNetwork, + }; + + const cardanoWalletInfo: Web3ProjectCardanoWallet = { + id: walletId, + projectId: this.sdk.projectId, + tags: options.tags || [], + key: encryptedKey, + pubKeyHash, + stakeCredentialHash, + }; + + const sparkWalletDev = new SparkWalletDeveloperControlled({ + sdk: this.sdk, + wallet: sparkWallet, + walletInfo: sparkWalletInfo + }); + + const cardanoWalletDev = new CardanoWalletDeveloperControlled({ + sdk: this.sdk, + wallet: cardanoWallet, + walletInfo: cardanoWalletInfo + }); + + this.spark = sparkWalletDev; + this.cardano = cardanoWalletDev; + + return { + info: walletData as MultiChainWalletInfo, + sparkWallet: sparkWalletDev, + cardanoWallet: cardanoWallet + }; + } + + throw new Error("Failed to create wallet"); + } + + /** + * Loads an existing developer-controlled wallet by ID and returns both chain instances. + * + * @param walletId - The wallet ID to load + * @returns Promise that resolves to both chain wallet instances + * + * @example + * ```typescript + * const { sparkWallet, cardanoWallet } = await sdk.wallet.initWallet("wallet-id"); + * + * // Use either wallet directly + * await sparkWallet.mintTokens(BigInt("1000000")); + * await cardanoWallet.sendAssets({...}); + * ``` + */ + async initWallet( + walletId: string + ): Promise<{ + info: MultiChainWalletInfo; + sparkWallet: SparkWalletDeveloperControlled; + cardanoWallet: MeshWallet; + }> { + if (!this.sdk.privateKey) { + throw new Error("Private key required to load developer-controlled wallet"); } - throw new Error("Failed to create multi-chain wallet"); + const walletInfo = await this.getProjectWallet(walletId); + const effectiveNetworkId = this.sdk.network === "mainnet" ? 1 : 0; + const sharedMnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: walletInfo.key, + }); + + const cardanoWallet = new MeshWallet({ + networkId: effectiveNetworkId, + key: { type: "mnemonic", words: sharedMnemonic.split(" ") }, + fetcher: this.sdk.providerFetcher, + submitter: this.sdk.providerSubmitter, + }); + await cardanoWallet.init(); + + const sparkNetwork = effectiveNetworkId === 1 ? "MAINNET" : "REGTEST"; + const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: sharedMnemonic, + options: { network: sparkNetwork }, + }); + + const sparkWalletInfo: Web3ProjectSparkWallet = { + id: walletId, + projectId: this.sdk.projectId, + tags: walletInfo.tags || [], + key: walletInfo.key, + publicKey: await sparkWallet.getIdentityPublicKey(), + network: sparkNetwork, + }; + + const cardanoWalletInfo: Web3ProjectCardanoWallet = { + id: walletId, + projectId: this.sdk.projectId, + tags: walletInfo.tags || [], + key: walletInfo.key, + pubKeyHash: walletInfo.chains?.cardano?.pubKeyHash || "", + stakeCredentialHash: walletInfo.chains?.cardano?.stakeCredentialHash || "", + }; + + const sparkWalletDev = new SparkWalletDeveloperControlled({ + sdk: this.sdk, + wallet: sparkWallet, + walletInfo: sparkWalletInfo + }); + + const cardanoWalletDev = new CardanoWalletDeveloperControlled({ + sdk: this.sdk, + wallet: cardanoWallet, + walletInfo: cardanoWalletInfo + }); + + this.spark = sparkWalletDev; + this.cardano = cardanoWalletDev; + + return { + info: walletInfo, + sparkWallet: sparkWalletDev, + cardanoWallet: cardanoWallet + }; } /** * Retrieves a multi-chain wallet with optional chain-specific loading. * * @param walletId - The unique identifier of the wallet - * @param networkId - Network ID (0 = testnet, 1 = mainnet) * @param chain - Optional specific chain to load (performance optimization) - * @param options - Additional chain-specific options * @returns Promise that resolves to multi-chain wallet instance * * @example * ```typescript * // Load specific chain - * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", 0, "spark"); + * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); * * // Load all available chains - * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id", 0, "cardano"); + * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); * ``` */ async getWallet( projectWalletId: string, - networkId: NetworkId, chain: SupportedChain, ): Promise { const walletInfo = await this.getProjectWallet(projectWalletId); @@ -182,18 +300,20 @@ export class WalletDeveloperControlled { info: walletInfo }; - let sharedMnemonic: string | null = null; + let mnemonic: string | null = null; if (this.sdk.privateKey) { - sharedMnemonic = await decryptWithPrivateKey({ + mnemonic = await decryptWithPrivateKey({ privateKey: this.sdk.privateKey, encryptedDataJSON: walletInfo.key, }); } - if ((chain === "cardano" || !chain) && walletInfo.chains.cardano && sharedMnemonic) { + const networkId = this.sdk.network === "mainnet" ? 1 : 0; + + if ((chain === "cardano" || !chain) && walletInfo.chains.cardano && mnemonic) { const cardanoWallet = new MeshWallet({ networkId: networkId, - key: { type: "mnemonic", words: sharedMnemonic.split(" ") }, + key: { type: "mnemonic", words: mnemonic.split(" ") }, fetcher: this.sdk.providerFetcher, submitter: this.sdk.providerSubmitter, }); @@ -202,28 +322,39 @@ export class WalletDeveloperControlled { instance.cardanoWallet = cardanoWallet; } - if ((chain === "spark" || !chain) && walletInfo.chains.spark && sharedMnemonic) { - const sparkNetwork = walletInfo.networkId === 1 ? "MAINNET" : "REGTEST"; + if ((chain === "spark" || !chain) && walletInfo.chains.spark && mnemonic) { + const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: sharedMnemonic, + mnemonicOrSeed: mnemonic, options: { network: sparkNetwork }, }); - instance.sparkWallet = sparkWallet; + const sparkWalletInfo: Web3ProjectSparkWallet = { + id: projectWalletId, + projectId: this.sdk.projectId, + tags: walletInfo.tags || [], + key: walletInfo.key, + publicKey: await sparkWallet.getIdentityPublicKey(), + network: sparkNetwork, + }; + + instance.sparkWallet = new SparkWalletDeveloperControlled({ + sdk: this.sdk, + wallet: sparkWallet, + walletInfo: sparkWalletInfo + }); } return instance; } /** - * Get the project's single multi-chain wallet + * Get a specific project wallet by ID */ - async getProjectWallet(walletId?: string): Promise { - const endpoint = walletId - ? `api/project-wallet/${this.sdk.projectId}/${walletId}` - : `api/project-wallet/${this.sdk.projectId}`; - - const { data, status } = await this.sdk.axiosInstance.get(endpoint); + async getProjectWallet(walletId: string): Promise { + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/${walletId}` + ); if (status === 200) { return data as MultiChainWalletInfo; @@ -231,4 +362,19 @@ export class WalletDeveloperControlled { throw new Error("Project wallet not found"); } + + /** + * Get all project wallets + */ + async getProjectWallets(): Promise { + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}` + ); + + if (status === 200) { + return data as MultiChainWalletInfo[]; + } + + throw new Error("Failed to get project wallets"); + } } diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark.ts index 37e3f5c..b0d3c8f 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark.ts @@ -1,686 +1,359 @@ -// TODO: Add network parameters to all methods and clean up API calls to consistently use network params - import { Web3Sdk } from ".."; -import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; -import { Web3ProjectSparkWallet, TokenCreationParams } from "../../types"; -import { v4 as uuidv4 } from "uuid"; -import { IssuerSparkWallet, IssuerTokenMetadata } from "@buildonspark/issuer-sdk"; -import { EmbeddedWallet } from "@meshsdk/bitcoin"; +import { decryptWithPrivateKey } from "../../functions"; +import { Web3ProjectSparkWallet } from "../../types"; +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; +import { extractIdentityPublicKey } from "../../chains/spark/utils"; import { - SparkMintTokensParams, - SparkBatchMintParams, - SparkTransferTokensParams, SparkFreezeTokensParams, SparkUnfreezeTokensParams, - SparkTransactionResult, - SparkBatchMintResult, - SparkTokenBalanceResult, - SparkFreezeResult, - SparkFrozenAddressesResult, - SparkTokenMetadataResponse, - PaginationParams, + SparkFreezeResult } from "../../types/spark/dev-wallet"; -import { QueryTokenTransactionsResponse } from "@buildonspark/spark-sdk/dist/proto/spark_token"; -import { extractIdentityPublicKey } from "../../chains/spark/utils"; /** - * SparkWalletDeveloperControlled - Manages Spark-specific developer-controlled wallets + * SparkWalletDeveloperControlled - Developer-controlled Spark wallet for token operations * - * This class provides functionality for managing developer-controlled Spark wallets, - * including wallet creation, token operations, and issuer wallet management. + * Provides token issuance, minting, transfer, and compliance operations on the Spark network. + * Wraps an IssuerSparkWallet instance with database synchronization. * * @example * ```typescript - * const sparkWallet = sdk.wallet.spark; - * const wallet = await sparkWallet.createWallet({ tags: ["my-wallet"] }); - * const tokenResult = await sparkWallet.createToken(wallet.id, { + * // Create wallet via SDK + * const { sparkWallet } = await sdk.wallet.createWallet({ + * tags: ["tokenization"] + * }); + * + * // Token operations + * await sparkWallet.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, * maxSupply: "1000000", * isFreezable: true * }); + * + * await sparkWallet.mintTokens(BigInt("1000000")); + * const balance = await sparkWallet.getTokenBalance(); * ``` */ export class SparkWalletDeveloperControlled { readonly sdk: Web3Sdk; - - constructor({ sdk }: { sdk: Web3Sdk }) { + private wallet: IssuerSparkWallet | null = null; + private walletInfo: Web3ProjectSparkWallet | null = null; + + constructor({ sdk, wallet, walletInfo }: { + sdk: Web3Sdk; + wallet?: IssuerSparkWallet; + walletInfo?: Web3ProjectSparkWallet; + }) { this.sdk = sdk; + this.wallet = wallet || null; + this.walletInfo = walletInfo || null; } /** - * Creates a new Spark wallet associated with the current project. - * - * @param params - Wallet creation parameters - * @param params.tags - Optional tags to organize the wallet - * @param params.network - Network to create the wallet on (default: "REGTEST") - * @returns Promise that resolves to both wallet info and ready-to-use IssuerSparkWallet instance - * - * @example - * ```typescript - * const { info, wallet } = await sparkWallet.createWallet({ - * tags: ["tokenization", "mainnet"], - * network: "MAINNET" - * }); - * - * // Use wallet instance immediately (no privateKey needed) - * const tokenTx = await wallet.createToken({ - * tokenName: "MyToken", - * tokenTicker: "MTK", - * decimals: 8, - * maxSupply: BigInt("1000000"), - * isFreezable: true - * }); - * ``` - */ - async createWallet({ - tags, - network = "REGTEST", - }: { - tags?: string[]; - network?: "MAINNET" | "REGTEST"; - } = {}): Promise<{ - info: Web3ProjectSparkWallet; - wallet: IssuerSparkWallet; - }> { - const project = await this.sdk.getProject(); - - if (!project.publicKey) { - throw new Error("Project public key not found"); - } - - const mnemonic = EmbeddedWallet.brew(256); - const encryptedMnemonic = await encryptWithPublicKey({ - publicKey: project.publicKey, - data: mnemonic.join(" "), - }); - - const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic.join(" "), - options: { network }, - }); - - const publicKey = await sparkWallet.getIdentityPublicKey(); - - const web3Wallet: Web3ProjectSparkWallet = { - id: uuidv4(), - key: encryptedMnemonic, - tags: tags || [], - projectId: this.sdk.projectId, - publicKey, - network, - }; - - const { data, status } = await this.sdk.axiosInstance.post( - `api/project-wallet/spark`, - web3Wallet, - ); - - if (status === 200) { - return { - info: data as Web3ProjectSparkWallet, - wallet: sparkWallet - }; - } - - throw new Error("Failed to create Spark wallet"); - } - - /** - * Retrieves all Spark wallets for the current project. - * - * @returns Promise that resolves to an array of all Spark wallets in the project - * - * @example - * ```typescript - * const wallets = await sparkWallet.getWallets(); - * console.log(`Found ${wallets.length} Spark wallets`); - * ``` + * Internal method to ensure wallet is loaded */ - async getWallets(): Promise { - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/spark`, - ); - - if (status === 200) { - return data as Web3ProjectSparkWallet[]; + private ensureWallet(): IssuerSparkWallet { + if (!this.wallet) { + throw new Error("Wallet not initialized. Use sdk.wallet.createWallet() or sdk.wallet.initWallet() first."); } - - throw new Error("Failed to get Spark wallets"); + return this.wallet; } /** - * Retrieves a specific Spark wallet by ID and creates a wallet instance. - * - * @param decryptKey - Whether to decrypt and return the mnemonic key (default: false) - * @returns Promise that resolves to wallet info and initialized IssuerSparkWallet instance - * - * @throws {Error} When private key is not found or wallet retrieval fails - * - * @example - * ```typescript - * const { info, wallet } = await sparkWallet.getWallet("wallet-id-123"); - * const address = await wallet.getSparkAddress(); - * ``` + * Internal helper to log token transactions to the database */ - async getWallet( - walletId: string, - network: "MAINNET" | "REGTEST" = "REGTEST", - decryptKey = false, - ): Promise<{ - info: Web3ProjectSparkWallet; - wallet: IssuerSparkWallet; - }> { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found"); - } - - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${network.toLowerCase()}`, - ); - - if (status === 200) { - const sparkWallet = data as Web3ProjectSparkWallet; - - const mnemonic = await decryptWithPrivateKey({ - privateKey: this.sdk.privateKey, - encryptedDataJSON: sparkWallet.key, - }); - - if (decryptKey) { - sparkWallet.key = mnemonic; - } - - const { wallet: issuerSparkWallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic, - options: { network: sparkWallet.network }, + private async logTransaction(params: { + tokenId: string; + type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + txHash?: string; + amount?: string; + fromAddress?: string; + toAddress?: string; + status?: string; + metadata?: Record; + }): Promise { + try { + await this.sdk.axiosInstance.post("/api/tokenization/transactions", { + tokenId: params.tokenId, + projectId: this.sdk.projectId, + projectWalletId: this.walletInfo?.id, + type: params.type, + chain: "spark", + network: this.walletInfo?.network.toLowerCase(), + txHash: params.txHash, + amount: params.amount, + fromAddress: params.fromAddress, + toAddress: params.toAddress, + status: params.status || "success", + metadata: params.metadata, }); - - return { info: sparkWallet, wallet: issuerSparkWallet }; - } - - throw new Error("Failed to get Spark wallet"); - } - - /** - * Get Spark wallets by tag - */ - async getWalletsByTag(tag: string): Promise { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found"); - } - - const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/spark/tag/${tag}`, - ); - - if (status === 200) { - return data as Web3ProjectSparkWallet[]; + } catch (error) { + console.warn(`Failed to log ${params.type} transaction:`, error); } - - throw new Error("Failed to get Spark wallets by tag"); } /** - * Get token metadata for tokens created by a specific issuer wallet. - * - * @returns Promise that resolves to token metadata information - * - * @throws {Error} When token metadata retrieval fails - * - * @example - * ```typescript - * const metadata = await sparkWallet.getIssuerTokenMetadata("wallet-123"); - * console.log(`Token: ${metadata.tokenName} (${metadata.tokenTicker})`); - * ``` + * Internal helper to get the token ID (hex) from wallet metadata */ - async getIssuerTokenMetadata(walletId: string): Promise { - const { wallet: sparkWallet } = await this.getWallet(walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - return await sparkWallet.getIssuerTokenMetadata(); + private async getTokenIdHex(): Promise { + const wallet = this.ensureWallet(); + const tokenMetadata = await wallet.getIssuerTokenMetadata(); + return Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); } - /** - * Creates a new token using a specific issuer wallet. - * - * @param params - Token creation parameters - * @param params.tokenName - The full name of the token - * @param params.tokenTicker - The ticker symbol for the token - * @param params.decimals - Number of decimal places for the token - * @param params.maxSupply - Maximum supply of tokens (optional, defaults to unlimited) - * @param params.isFreezable - Whether token transfers can be frozen - * @returns Promise that resolves to transaction information - * - * @throws {Error} When token creation fails + * Creates a new token on the Spark network using this wallet as the issuer. * - * @example - * ```typescript - * const result = await sparkWallet.createToken({ - * tokenName: "My Token", - * tokenTicker: "MTK", - * decimals: 8, - * maxSupply: "1000000", - * isFreezable: true, - * walletId: "wallet-123" - * }); - * console.log(`Token created with transaction: ${result.transactionId}`); - * ``` + * @param params Token creation parameters (matches IssuerSparkWallet.createToken) + * @returns Promise resolving to the transaction ID + * @throws Error if wallet is not initialized or token creation fails */ - async createToken(params: TokenCreationParams & { walletId: string }): Promise { - const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - - const transactionId = await sparkWallet.createToken({ - tokenName: params.tokenName, - tokenTicker: params.tokenTicker, - decimals: params.decimals, - maxSupply: params.maxSupply ? BigInt(params.maxSupply) : undefined, - isFreezable: params.isFreezable, - }); + async createToken(params: { + tokenName: string; + tokenTicker: string; + decimals: number; + maxSupply?: bigint; + isFreezable: boolean; + }): Promise { + const wallet = this.ensureWallet(); + const transactionId = await wallet.createToken(params); try { - const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); - const rawTokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); - - await this.sdk.axiosInstance.post('/api/tokenization/tokens', { + const tokenMetadata = await wallet.getIssuerTokenMetadata(); + const rawTokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + // Create tokenization policy + await this.sdk.axiosInstance.post("/api/tokenization/tokens", { tokenId: rawTokenIdHex, projectId: this.sdk.projectId, - walletId: walletInfo.id, + walletId: this.walletInfo?.id, chain: "spark", - network: walletInfo.network.toLowerCase() + network: this.walletInfo?.network.toLowerCase(), + }); + + // Log the create transaction + await this.logTransaction({ + tokenId: rawTokenIdHex, + type: "create", + txHash: transactionId, + metadata: { + tokenName: params.tokenName, + tokenTicker: params.tokenTicker, + decimals: params.decimals, + maxSupply: params.maxSupply?.toString(), + isFreezable: params.isFreezable, + }, }); } catch (saveError) { console.warn("Failed to save token to main app:", saveError); } - return { - transactionId, - }; + return transactionId; } - /** - * Mints tokens to a specified address or to the issuer wallet if no address provided. - * - * When an address is provided, this method performs a two-step process: - * 1. Mints tokens to the issuer wallet - * 2. Transfers the minted tokens to the specified address - * - * @param params - Minting parameters - * @param params.tokenizationId - The Bech32m token identifier to mint - * @param params.amount - The amount of tokens to mint (as string to handle large numbers) - * @param params.address - Optional Spark address to receive tokens (defaults to issuer wallet) - * @returns Promise that resolves to transaction information (transfer tx if address provided, mint tx otherwise) - * - * @throws {Error} When token minting or transfer fails + * Mints tokens from this issuer wallet. * - * @example - * ```typescript - * // Mint to issuer wallet - * const result1 = await sparkWallet.mintTokens({ - * tokenizationId: "btkn1token123...", - * amount: "1000000", - * walletId: "wallet-123" - * }); - * - * // Mint and transfer to specific address (two-step process) - * const result2 = await sparkWallet.mintTokens({ - * tokenizationId: "btkn1token123...", - * amount: "1000000", - * address: "spark1recipient456...", - * walletId: "wallet-123" - * }); - * ``` + * @param amount Amount of tokens to mint (as bigint) + * @returns Promise resolving to the transaction ID + * @throws Error if wallet is not initialized or minting fails */ - async mintTokens(params: SparkMintTokensParams & { walletId: string }): Promise { - const { wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - - if (params.address) { - // Two-step process: mint to issuer, then transfer to target address - await sparkWallet.mintTokens(BigInt(params.amount)); - - // Transfer the minted tokens to the specified address - const transferResult = await sparkWallet.transferTokens({ - tokenIdentifier: params.tokenizationId, - tokenAmount: BigInt(params.amount), - receiverSparkAddress: params.address, - }); - - return { - transactionId: transferResult, - }; - } else { - const mintResult = await sparkWallet.mintTokens(BigInt(params.amount)); + async mintTokens(amount: bigint): Promise { + const wallet = this.ensureWallet(); + const txHash = await wallet.mintTokens(amount); + + // Log the mint transaction + const tokenId = await this.getTokenIdHex(); + await this.logTransaction({ + tokenId, + type: "mint", + txHash, + amount: amount.toString(), + }); - return { - transactionId: mintResult, - }; - } + return txHash; } /** - * Efficiently mints tokens to multiple recipients in batch. - * - * This method optimizes the minting process by: - * 1. Calculating the total amount needed for all recipients - * 2. Performing a single mint operation to the issuer wallet - * 3. Executing a single batch transfer to all recipients simultaneously - * - * @param params - Batch minting parameters - * @param params.tokenizationId - The Bech32m token identifier to mint - * @param params.recipients - Array of recipient addresses and amounts - * @returns Promise that resolves to mint transaction ID and batch transfer transaction ID + * Transfers tokens from this wallet to another Spark address. * - * @throws {Error} When token minting or batch transfer fails - * - * @example - * ```typescript - * const result = await sparkWallet.batchMintTokens("wallet-id", { - * tokenizationId: "spark1token123...", - * recipients: [ - * { address: "spark1addr1...", amount: "1000" }, - * { address: "spark1addr2...", amount: "2000" }, - * { address: "spark1addr3...", amount: "500" } - * ] - * }); - * console.log(`Mint tx: ${result.mintTransactionId}`); - * console.log(`Batch transfer tx: ${result.batchTransferTransactionId}`); - * ``` + * @param params Transfer parameters including token identifier, amount, and destination + * @returns Promise resolving to the transaction ID + * @throws Error if wallet is not initialized or transfer fails */ - async batchMintTokens( - params: SparkBatchMintParams & { walletId: string } - ): Promise { - const { wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - - // Calculate total amount needed for all recipients - const totalAmount = params.recipients.reduce( - (sum, recipient) => sum + BigInt(recipient.amount), - 0n - ); - - const mintTransactionId = await sparkWallet.mintTokens(totalAmount); - const receiverOutputs = params.recipients.map(recipient => ({ - tokenIdentifier: params.tokenizationId, - tokenAmount: BigInt(recipient.amount), - receiverSparkAddress: recipient.address, - })); + async transferTokens(params: { + tokenIdentifier: string; + amount: bigint; + toAddress: string; + }): Promise { + const wallet = this.ensureWallet(); + const txHash = await wallet.transferTokens({ + tokenIdentifier: params.tokenIdentifier as Bech32mTokenIdentifier, + tokenAmount: params.amount, + receiverSparkAddress: params.toAddress, + }); - const batchTransferTransactionId = await sparkWallet.batchTransferTokens(receiverOutputs); + // Log the transfer transaction + const tokenId = await this.getTokenIdHex(); + const issuerAddress = await wallet.getSparkAddress(); + await this.logTransaction({ + tokenId, + type: "transfer", + txHash, + amount: params.amount.toString(), + fromAddress: issuerAddress, + toAddress: params.toAddress, + }); - return { - mintTransactionId, - batchTransferTransactionId, - }; + return txHash; } /** - * Transfers tokens from an issuer wallet to another Spark address. - * - * @param params - Transfer parameters - * @param params.tokenIdentifier - The Bech32m token identifier for the token to transfer - * @param params.amount - The amount of tokens to transfer (as string to handle large numbers) - * @param params.toAddress - The recipient Spark address - * @returns Promise that resolves to transaction information - * - * @throws {Error} When token transfer fails + * Burns tokens permanently from circulation. * - * @example - * ```typescript - * const result = await sparkWallet.transferTokens({ - * tokenIdentifier: "btkn1abc...", - * amount: "100000", - * toAddress: "spark1def...", - * walletId: "wallet-123" - * }); - * console.log(`Transferred tokens with transaction: ${result.transactionId}`); - * ``` + * @param amount Amount of tokens to burn (as bigint) + * @returns Promise resolving to the transaction ID + * @throws Error if wallet is not initialized or burning fails */ - async transferTokens( - params: SparkTransferTokensParams & { walletId: string } - ): Promise { - const { wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - - const result = await sparkWallet.transferTokens({ - tokenIdentifier: params.tokenIdentifier, - tokenAmount: BigInt(params.amount), - receiverSparkAddress: params.toAddress, + async burnTokens(amount: bigint): Promise { + const wallet = this.ensureWallet(); + const txHash = await wallet.burnTokens(amount); + + // Log the burn transaction + const tokenId = await this.getTokenIdHex(); + await this.logTransaction({ + tokenId, + type: "burn", + txHash, + amount: amount.toString(), }); - return { - transactionId: result, - }; + return txHash; } /** - * Retrieves token balance for a specific address using Sparkscan API. + * Gets the token balance for this issuer wallet. * - * @param params - Balance query parameters - * @param params.tokenId - The token identifier to check balance for - * @param params.address - The Spark address to check balance of - * @returns Promise that resolves to balance information - * - * @throws {Error} When balance retrieval fails - * - * @example - * ```typescript - * const balance = await sparkWallet.getTokenBalance({ - * tokenId: "spark1token123...", - * address: "spark1addr456..." - * }); - * console.log(`Balance: ${balance.balance} tokens`); - * ``` + * @returns Promise resolving to balance information + * @throws Error if wallet is not initialized */ - async getTokenBalance(walletId: string): Promise { - try { - const { wallet: sparkWallet } = await this.getWallet(walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not found"); - } - - const balanceResult = await sparkWallet.getIssuerTokenBalance(); - - return { balance: balanceResult.balance.toString() }; - } catch (error) { - throw new Error(`Failed to get token balance: ${error}`); - } + async getTokenBalance() { + const wallet = this.ensureWallet(); + const result = await wallet.getIssuerTokenBalance(); + return { balance: result.balance.toString() }; } /** - * Query token transactions using Sparkscan API - * @param params - Query parameters for filtering transactions - * @param params.tokenIdentifiers - Array of token identifiers to filter by - * @param params.ownerPublicKeys - Optional array of owner public keys to filter by - * @param params.issuerPublicKeys - Optional array of issuer public keys to filter by - * @param params.tokenTransactionHashes - Optional array of transaction hashes to filter by - * @param params.outputIds - Optional array of output IDs to filter by - * @returns Promise resolving to array of token transactions + * Gets metadata for tokens created by this issuer wallet. * - * @example - * ```typescript - * const transactions = await sparkWallet.queryTokenTransactions({ - * tokenIdentifiers: ["btkn1..."], - * ownerPublicKeys: ["spark1..."] - * }); - * console.log("Token transactions:", transactions); - * ``` + * @returns Promise resolving to token metadata + * @throws Error if wallet is not initialized */ - async queryTokenTransactions(params: { - walletId: string; - sparkAddresses?: string[]; - ownerPublicKeys?: string[]; - issuerPublicKeys?: string[]; - tokenTransactionHashes?: string[]; - tokenIdentifiers?: string[]; - outputIds?: string[]; - order?: "asc" | "desc"; - pageSize?: number; - offset?: number; - }): Promise { - try { - const { wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not found"); - } - - return await sparkWallet.queryTokenTransactions(params); - } catch (error) { - throw new Error(`Failed to query token transactions: ${error}`); - } + async getTokenMetadata() { + const wallet = this.ensureWallet(); + return await wallet.getIssuerTokenMetadata(); } /** - * TODO: Remove this, we should use the issuer sdk - * Get metadata for a specific token. + * Freezes tokens at a specific Spark address for compliance purposes. * - * @param params - Token metadata query parameters - * @param params.tokenIds - Array of token identifiers (up to 100 tokens) - * @returns Promise that resolves to token metadata response - * - * @example - * ```typescript - * // Single token - * const response = await sparkWallet.getTokenMetadata({ tokenIds: ["btkn1token123..."] }); - * const metadata = response.metadata[0]; - * console.log(`${metadata.name} (${metadata.ticker})`); - * - * // Multiple tokens - * const batchResponse = await sparkWallet.getTokenMetadata({ - * tokenIds: ["btkntoken1...", "btkn1token2...", "btkn1token3..."] - * }); - * batchResponse.metadata.forEach(token => console.log(token.name)); - * ``` + * @param params Freeze parameters including address and optional reason + * @returns Promise resolving to freeze operation results + * @throws Error if wallet is not initialized or freeze operation fails */ - async getTokenMetadata(params: { tokenIds: string[] }): Promise { - if (params.tokenIds.length === 0) { - return { metadata: [], total_count: 0 }; - } + async freezeTokens(params: SparkFreezeTokensParams): Promise { + const wallet = this.ensureWallet(); + const result = await wallet.freezeTokens(params.address); - if (params.tokenIds.length > 100) { - throw new Error("Maximum 100 token IDs allowed per batch request"); - } + // Save freeze operation to database + try { + const tokenMetadata = await wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const publicKeyHash = extractIdentityPublicKey(params.address); - const { data, status } = await this.sdk.axiosInstance.post( - `api/spark/tokens/metadata/batch`, - { - token_addresses: params.tokenIds + if (!publicKeyHash) { + throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); } - ); - if (status === 200) { - return data as SparkTokenMetadataResponse; - } - - throw new Error("Failed to get token metadata"); - } - - /** - * Burns tokens permanently from circulation. - * - * @param params - Token burning parameters - * @param params.amount - The amount of tokens to burn from issuer wallet - * @returns Promise that resolves to transaction information - * - * @throws {Error} When token burning fails - * - * @example - * ```typescript - * const result = await sparkWallet.burnTokens({ - * amount: "1000" - * }); - * console.log(`Burned tokens with transaction: ${result.transactionId}`); - * ``` - */ - async burnTokens(params: { amount: string; walletId: string }): Promise { - const { wallet: sparkWallet } = await this.getWallet(params.walletId); + // Update frozen addresses table + await this.sdk.axiosInstance.post("/api/tokenization/frozen-addresses", { + tokenId, + projectId: this.sdk.projectId, + projectWalletId: this.walletInfo?.id, + chain: "spark", + network: this.walletInfo?.network.toLowerCase(), + publicKeyHash, + isFrozen: true, + freezeReason: params.freezeReason || "Frozen by issuer", + frozenAt: new Date().toISOString(), + }); - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); + // Log the freeze transaction + await this.logTransaction({ + tokenId, + type: "freeze", + toAddress: params.address, + amount: result.impactedTokenAmount.toString(), + metadata: { + freezeReason: params.freezeReason, + impactedOutputIds: result.impactedOutputIds, + publicKeyHash, + }, + }); + } catch (saveError) { + console.warn("Failed to save freeze operation:", saveError); } - const result = await sparkWallet.burnTokens(BigInt(params.amount)); - return { - transactionId: result, + impactedOutputIds: result.impactedOutputIds, + impactedTokenAmount: result.impactedTokenAmount.toString(), }; } /** - * Freezes all tokens at a specific Spark address (compliance/admin control). - * - * This operation can only be performed by issuer wallets on freezable tokens. - * Frozen tokens cannot be transferred until unfrozen by the issuer. - * - * @param params - Freeze parameters - * @param params.address - The Spark address to freeze tokens at - * @returns Promise that resolves to freeze operation details + * Unfreezes tokens at a specific Spark address. * - * @throws {Error} When token freezing fails or tokens are not freezable - * - * @example - * ```typescript - * const result = await sparkWallet.freezeTokens({ - * address: "spark1suspicious123...", - * walletId: "wallet-123" - * }); - * console.log(`Frozen ${result.impactedTokenAmount} tokens at ${result.impactedOutputIds.length} outputs`); - * ``` + * @param params Unfreeze parameters including the address to unfreeze + * @returns Promise resolving to unfreeze operation results + * @throws Error if wallet is not initialized or unfreeze operation fails */ - async freezeTokens( - params: SparkFreezeTokensParams & { walletId: string } - ): Promise { - const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); - } - - const result = await sparkWallet.freezeTokens(params.address); + async unfreezeTokens(params: SparkUnfreezeTokensParams): Promise { + const wallet = this.ensureWallet(); + const result = await wallet.unfreezeTokens(params.address); + // Update freeze status in database try { - const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); - - // Extract public key hash from Spark address + const tokenMetadata = await wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); const publicKeyHash = extractIdentityPublicKey(params.address); + if (!publicKeyHash) { throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); } - await this.sdk.axiosInstance.post('/api/tokenization/frozen-addresses', { + // Update frozen addresses table + await this.sdk.axiosInstance.put("/api/tokenization/frozen-addresses", { tokenId, + publicKeyHash, projectId: this.sdk.projectId, - projectWalletId: walletInfo.id, - chain: "spark", - network: walletInfo.network.toLowerCase(), - publicKeyHash: publicKeyHash, - isFrozen: true, - freezeReason: params.freezeReason || "Frozen by issuer", - frozenAt: new Date().toISOString() + projectWalletId: this.walletInfo?.id, + }); + + // Log the unfreeze transaction + await this.logTransaction({ + tokenId, + type: "unfreeze", + toAddress: params.address, + amount: result.impactedTokenAmount.toString(), + metadata: { + impactedOutputIds: result.impactedOutputIds, + publicKeyHash, + }, }); } catch (saveError) { - console.warn("Failed to save freeze operation to main app:", saveError); + console.warn("Failed to save unfreeze operation:", saveError); } return { @@ -689,120 +362,102 @@ export class SparkWalletDeveloperControlled { }; } + /** - * Unfreezes all tokens at a specific Spark address (compliance/admin control). - * - * This operation can only be performed by issuer wallets that previously froze the tokens. - * Unfrozen tokens can be transferred normally again. - * - * @param params - Unfreeze parameters - * @param params.address - The Spark address to unfreeze tokens at - * @returns Promise that resolves to unfreeze operation details + * Gets an existing Spark wallet by ID and returns a SparkWalletDeveloperControlled instance. * - * @throws {Error} When token unfreezing fails + * @param walletId The wallet ID to retrieve + * @returns Promise resolving to a SparkWalletDeveloperControlled instance * * @example * ```typescript - * const result = await sparkWallet.unfreezeTokens({ - * address: "spark1cleared123...", - * walletId: "wallet-123" - * }); - * console.log(`Unfrozen ${result.impactedTokenAmount} tokens at ${result.impactedOutputIds.length} outputs`); + * const sparkWallet = await sdk.wallet.spark.getWallet("existing-wallet-id"); + * const address = await sparkWallet.getAddress(); + * await sparkWallet.mintTokens(BigInt("1000000")); * ``` */ - async unfreezeTokens( - params: SparkUnfreezeTokensParams & { walletId: string } - ): Promise { - const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId); - - if (!sparkWallet) { - throw new Error("Spark wallet not available for this project"); + async getWallet(walletId: string): Promise { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found - required to decrypt wallet"); } - const result = await sparkWallet.unfreezeTokens(params.address); + const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, + ); - try { - const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); + if (status === 200) { + const sparkWalletInfo = data as Web3ProjectSparkWallet; - const publicKeyHash = extractIdentityPublicKey(params.address); - if (!publicKeyHash) { - throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); - } + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: sparkWalletInfo.key, + }); - await this.sdk.axiosInstance.put('/api/tokenization/frozen-addresses', { - tokenId, - publicKeyHash: publicKeyHash, - projectId: this.sdk.projectId, - projectWalletId: walletInfo.id, + const { wallet: issuerSparkWallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic, + options: { network: sparkWalletInfo.network }, + }); + + return new SparkWalletDeveloperControlled({ + sdk: this.sdk, + wallet: issuerSparkWallet, + walletInfo: sparkWalletInfo }); - } catch (saveError) { - console.warn("Failed to save unfreeze operation to main app:", saveError); } - return { - impactedOutputIds: result.impactedOutputIds, - impactedTokenAmount: result.impactedTokenAmount.toString(), - }; + throw new Error("Failed to get Spark wallet"); } /** - * Retrieves frozen addresses with pagination support for administrative review. - * - * This method queries the backend service to get a paginated list of addresses - * that currently have frozen tokens. Useful for compliance dashboards and admin tables. + * Lists all Spark wallets for the current project. + * Returns basic wallet information for selection/management purposes. * - * @param params - Optional pagination parameters - * @param params.page - Page number to retrieve (1-based, default: 1) - * @param params.limit - Number of items per page (default: 50) - * @param params.offset - Number of items to skip (alternative to page) - * @returns Promise that resolves to paginated list of frozen addresses with metadata - * - * @throws {Error} When frozen address query fails + * @returns Promise resolving to array of wallet information * * @example * ```typescript - * // Get first page with default limit - * const frozenInfo = await sparkWallet.getFrozenAddresses(); - * - * // Get specific page with custom limit - * const page2 = await sparkWallet.getFrozenAddresses({ page: 2, limit: 25 }); + * const wallets = await sdk.wallet.spark.list(); + * console.log(`Found ${wallets.length} Spark wallets:`); + * wallets.forEach(w => console.log(`- ${w.id}: tags=[${w.tags.join(', ')}]`)); * - * // Display in admin table - * frozenInfo.frozenAddresses.forEach(addr => { - * console.log(`${addr.address}: ${addr.frozenTokenAmount} tokens frozen since ${addr.frozenAt}`); - * }); - * console.log(`Page ${frozenInfo.pagination.currentPage} of ${frozenInfo.pagination.totalPages}`); + * // Load a specific wallet for operations + * const wallet = await sdk.wallet.spark.getWallet(wallets[0].id); * ``` */ - async getFrozenAddresses(params?: PaginationParams & { walletId: string }): Promise { - if (!params?.walletId) { - throw new Error("walletId is required for getFrozenAddresses"); + async list(): Promise { + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/spark`, + ); + + if (status === 200) { + return data as Web3ProjectSparkWallet[]; } - const targetNetwork = params.network || "REGTEST"; - const { info: walletInfo, wallet: sparkWallet } = await this.getWallet(params.walletId, targetNetwork); - const tokenMetadata = await sparkWallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString('hex'); - - const queryParams = new URLSearchParams({ - tokenId: tokenId, - projectId: this.sdk.projectId, - chain: "spark", - network: walletInfo.network.toLowerCase(), - ...(params?.page && { page: params.page.toString() }), - ...(params?.limit && { limit: params.limit.toString() }), - ...(params?.offset && { offset: params.offset.toString() }), - }); + throw new Error("Failed to get Spark wallets"); + } + /** + * Gets Spark wallets filtered by tag. + * + * @param tag The tag to filter by + * @returns Promise resolving to array of matching wallet information + * + * @example + * ```typescript + * const tokenizationWallets = await sdk.wallet.spark.getByTag("tokenization"); + * const wallet = await sdk.wallet.spark.getWallet(tokenizationWallets[0].id); + * ``` + */ + async getByTag(tag: string): Promise { const { data, status } = await this.sdk.axiosInstance.get( - `api/tokenization/frozen-addresses?${queryParams.toString()}` + `api/project-wallet/${this.sdk.projectId}/spark/tag/${tag}`, ); if (status === 200) { - return data as SparkFrozenAddressesResult; + return data as Web3ProjectSparkWallet[]; } - throw new Error("Failed to get frozen addresses"); + throw new Error("Failed to get Spark wallets by tag"); } } \ No newline at end of file diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index cb1ac9d..3779cc3 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -1,5 +1,5 @@ import { MeshWallet } from "@meshsdk/wallet"; -import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import type { SparkWalletDeveloperControlled } from "../../sdk/wallet-developer-controlled/spark"; /** * Standardized network ID type (0 = testnet, 1 = mainnet) @@ -12,7 +12,6 @@ export type NetworkId = 0 | 1; export interface MultiChainWalletOptions { tags?: string[]; networkId?: NetworkId; - chains?: ("cardano" | "spark")[]; } /** @@ -31,9 +30,7 @@ export interface MultiChainWalletInfo { }; spark?: { mainnetPublicKey: string; - mainnetAddress: string; regtestPublicKey: string; - regtestAddress: string; }; }; createdAt: string; @@ -45,7 +42,7 @@ export interface MultiChainWalletInfo { export interface MultiChainWalletInstance { info: MultiChainWalletInfo; cardanoWallet?: MeshWallet; - sparkWallet?: IssuerSparkWallet; + sparkWallet?: SparkWalletDeveloperControlled; } /** diff --git a/src/types/spark/dev-wallet.ts b/src/types/spark/dev-wallet.ts index 30618fe..9023531 100644 --- a/src/types/spark/dev-wallet.ts +++ b/src/types/spark/dev-wallet.ts @@ -1,86 +1,34 @@ -import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; - -/** - * Enhanced minting parameters for dev-controlled wallets (extends base MintTokenParams) - */ -export interface SparkMintTokensParams { - tokenizationId: Bech32mTokenIdentifier; - amount: string; - address?: string; -} - -/** - * Individual recipient for batch minting operations - */ -export interface SparkBatchRecipient { - address: string; - amount: string; -} - -/** - * Parameters for batch minting Spark tokens to multiple recipients - */ -export interface SparkBatchMintParams { - tokenizationId: Bech32mTokenIdentifier; - recipients: SparkBatchRecipient[]; -} - -/** - * Parameters for transferring Spark tokens - */ -export interface SparkTransferTokensParams { - tokenIdentifier: Bech32mTokenIdentifier; - amount: string; - toAddress: string; -} - -/** - * Parameters for querying token balance - */ -export interface SparkTokenBalanceParams { - tokenId: string; - address?: string; -} - -/** - * Standard transaction result for Spark operations - */ -export interface SparkTransactionResult { - transactionId: string; -} - /** - * Result for batch minting operations + * Developer-controlled Spark wallet types + * + * Most types are re-exported from @buildonspark/spark-sdk and @buildonspark/issuer-sdk. + * This file only contains types specific to our wrapper layer. */ -export interface SparkBatchMintResult { - mintTransactionId: string; - batchTransferTransactionId: string; -} -/** - * Result for token balance queries - */ -export interface SparkTokenBalanceResult { - balance: string; -} +// Re-export SDK types for convenience +export type { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; +export type { IssuerTokenMetadata } from "@buildonspark/issuer-sdk"; /** - * Parameters for freezing tokens at a specific address + * Parameters for freezing tokens at a specific address. + * Extends SDK freeze with optional reason for database tracking. */ export interface SparkFreezeTokensParams { address: string; + /** Optional reason for freezing - stored in database for compliance tracking */ freezeReason?: string; } /** - * Parameters for unfreezing tokens at a specific address + * Parameters for unfreezing tokens at a specific address. */ export interface SparkUnfreezeTokensParams { address: string; } /** - * Result for freeze/unfreeze operations + * Result for freeze/unfreeze operations. + * Matches SDK return type but with string amounts for serialization. */ export interface SparkFreezeResult { impactedOutputIds: string[]; @@ -88,68 +36,12 @@ export interface SparkFreezeResult { } /** - * Information about a frozen address + * Information about a frozen address stored in database. */ export interface SparkFrozenAddressInfo { address: string; frozenTokenAmount: string; freezeTransactionId?: string; + freezeReason?: string; frozenAt: string; } - -/** - * Pagination parameters for queries - */ -export interface PaginationParams { - page?: number; - limit?: number; - offset?: number; - network?: "MAINNET" | "REGTEST"; -} - -/** - * Pagination metadata in results - */ -export interface PaginationMeta { - totalItems: number; - currentPage: number; - totalPages: number; - limit: number; - hasNextPage: boolean; - hasPreviousPage: boolean; -} - -/** - * Result for querying frozen addresses with pagination - */ -export interface SparkFrozenAddressesResult { - frozenAddresses: SparkFrozenAddressInfo[]; - pagination: PaginationMeta; -} - -/** - * Token metadata from Sparkscan API - */ -export interface SparkTokenMetadata { - tokenIdentifier: string; - tokenAddress: string; - name: string; - ticker: string; - decimals: number; - issuerPublicKey: string; - iconUrl: string; - holderCount: number; - priceUsd: number; - maxSupply: number | null; - isFreezable: boolean | null; - createdAt: string | null; - updatedAt: string | null; -} - -/** - * Response for token metadata batch query - */ -export interface SparkTokenMetadataResponse { - metadata: SparkTokenMetadata[]; - total_count: number; -} \ No newline at end of file From 72fc321dc279e72900eec08f889f029e771f5490 Mon Sep 17 00:00:00 2001 From: Jingles Date: Wed, 26 Nov 2025 10:57:06 +0800 Subject: [PATCH 13/30] feedback and improve spark tokens --- src/sdk/index.ts | 5 + src/sdk/tokenization/index.ts | 14 +++ src/sdk/tokenization/spark.ts | 53 ++++++++ .../wallet-developer-controlled/cardano.ts | 84 +++++++++---- src/sdk/wallet-developer-controlled/index.ts | 119 ++++++++++-------- .../{spark.ts => spark-issuer.ts} | 6 +- src/types/core/multi-chain.ts | 4 +- 7 files changed, 204 insertions(+), 81 deletions(-) create mode 100644 src/sdk/tokenization/index.ts create mode 100644 src/sdk/tokenization/spark.ts rename src/sdk/wallet-developer-controlled/{spark.ts => spark-issuer.ts} (98%) diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 119dd62..241fc23 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -3,6 +3,7 @@ import { WalletDeveloperControlled } from "./wallet-developer-controlled/"; import { Web3Project } from "../types"; import { IFetcher, ISubmitter } from "@meshsdk/common"; import { Sponsorship } from "./sponsorship"; +import { Tokenization } from "./tokenization"; export const meshUniversalStaticUtxo = { mainnet: { @@ -93,6 +94,7 @@ export class Web3Sdk { project: Web3Project | undefined; wallet: WalletDeveloperControlled; sponsorship: Sponsorship; + tokenization: Tokenization; constructor({ appUrl, @@ -130,6 +132,9 @@ export class Web3Sdk { this.sponsorship = new Sponsorship({ sdk: this, }); + this.tokenization = new Tokenization({ + sdk: this, + }); } async getProject() { diff --git a/src/sdk/tokenization/index.ts b/src/sdk/tokenization/index.ts new file mode 100644 index 0000000..94c4887 --- /dev/null +++ b/src/sdk/tokenization/index.ts @@ -0,0 +1,14 @@ +import { Web3Sdk } from ".."; +import { TokenizationSpark } from "./spark"; + +export class Tokenization { + private readonly sdk: Web3Sdk; + spark: TokenizationSpark; + + constructor({ sdk }: { sdk: Web3Sdk }) { + { + this.sdk = sdk; + this.spark = new TokenizationSpark({ sdk: this.sdk }); + } + } +} diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts new file mode 100644 index 0000000..0580927 --- /dev/null +++ b/src/sdk/tokenization/spark.ts @@ -0,0 +1,53 @@ +import { Web3Sdk } from ".."; + +/** + * TODO: + * - Replace with actual implementation + * - provide correct inputs + * - define output types for all methods + * - integrate API request if need database calls, e.g. get token policies from database + */ + +export class TokenizationSpark { + private readonly sdk: Web3Sdk; + + constructor({ sdk }: { sdk: Web3Sdk }) { + { + this.sdk = sdk; + } + } + + async createToken({}): Promise { + return ""; + } + + async mintTokens({}): Promise { + return ""; + } + + async getTokenBalance(address?: string) {} + + async getTokenMetadata(): Promise<{}> { + return {}; + } + + async transferTokens({}): Promise { + return ""; + } + + async batchTransferTokens({}): Promise<{}> { + return {}; + } + + async freezeTokens(address: string): Promise<{}> { + return {}; + } + + async unfreezeTokens(address: string): Promise<{}> { + return {}; + } + + async burnTokens(quantity: number): Promise { + return ""; + } +} diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index ff9998a..0606a94 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -1,9 +1,7 @@ import { Web3Sdk } from ".."; import { MeshWallet } from "@meshsdk/wallet"; -import { decryptWithPrivateKey, encryptWithPublicKey } from "../../functions"; +import { decryptWithPrivateKey } from "../../functions"; import { Web3ProjectCardanoWallet, TokenCreationParams } from "../../types"; -import { deserializeBech32Address } from "@meshsdk/core-cst"; -import { v4 as uuidv4 } from "uuid"; import { CardanoTransactionResult, CardanoTokenBalanceParams, @@ -24,8 +22,12 @@ export class CardanoWalletDeveloperControlled { private wallet: MeshWallet | null = null; private walletInfo: Web3ProjectCardanoWallet | null = null; - constructor({ sdk, wallet, walletInfo }: { - sdk: Web3Sdk; + constructor({ + sdk, + wallet, + walletInfo, + }: { + sdk: Web3Sdk; wallet?: MeshWallet; walletInfo?: Web3ProjectCardanoWallet; }) { @@ -139,8 +141,12 @@ export class CardanoWalletDeveloperControlled { * }); * ``` */ - async createToken(params: TokenCreationParams): Promise { - throw new Error("Cardano token creation not implemented yet - awaiting CIP-113 standard"); + async createToken( + params: TokenCreationParams, + ): Promise { + throw new Error( + "Cardano token creation not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -151,8 +157,14 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async mintTokens(params: { tokenId: string; amount: string; address?: string }): Promise { - throw new Error("Cardano token minting not implemented yet - awaiting CIP-113 standard"); + async mintTokens(params: { + tokenId: string; + amount: string; + address?: string; + }): Promise { + throw new Error( + "Cardano token minting not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -163,8 +175,12 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async transferTokens(params: CardanoTransferTokensParams): Promise { - throw new Error("Cardano token transfer not implemented yet - awaiting CIP-113 standard"); + async transferTokens( + params: CardanoTransferTokensParams, + ): Promise { + throw new Error( + "Cardano token transfer not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -175,8 +191,12 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async batchTransferTokens(params: CardanoBatchTransferParams): Promise { - throw new Error("Cardano batch token transfer not implemented yet - awaiting CIP-113 standard"); + async batchTransferTokens( + params: CardanoBatchTransferParams, + ): Promise { + throw new Error( + "Cardano batch token transfer not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -187,8 +207,12 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async getTokenBalance(params: CardanoTokenBalanceParams): Promise { - throw new Error("Cardano token balance query not implemented yet - awaiting CIP-113 standard"); + async getTokenBalance( + params: CardanoTokenBalanceParams, + ): Promise { + throw new Error( + "Cardano token balance query not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -200,7 +224,9 @@ export class CardanoWalletDeveloperControlled { * @throws {Error} Method not implemented yet - awaiting CIP-113 */ async getTokenMetadata(params: { tokenId: string }): Promise { - throw new Error("Cardano token metadata query not implemented yet - awaiting CIP-113 standard"); + throw new Error( + "Cardano token metadata query not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -211,8 +237,12 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async burnTokens(params: CardanoBurnTokensParams): Promise { - throw new Error("Cardano token burning not implemented yet - awaiting CIP-113 standard"); + async burnTokens( + params: CardanoBurnTokensParams, + ): Promise { + throw new Error( + "Cardano token burning not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -223,8 +253,12 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async freezeTokens(params: CardanoFreezeTokensParams): Promise { - throw new Error("Cardano token freezing not implemented yet - awaiting CIP-113 standard"); + async freezeTokens( + params: CardanoFreezeTokensParams, + ): Promise { + throw new Error( + "Cardano token freezing not implemented yet - awaiting CIP-113 standard", + ); } /** @@ -235,7 +269,11 @@ export class CardanoWalletDeveloperControlled { * * @throws {Error} Method not implemented yet - awaiting CIP-113 */ - async unfreezeTokens(params: CardanoUnfreezeTokensParams): Promise { - throw new Error("Cardano token unfreezing not implemented yet - awaiting CIP-113 standard"); + async unfreezeTokens( + params: CardanoUnfreezeTokensParams, + ): Promise { + throw new Error( + "Cardano token unfreezing not implemented yet - awaiting CIP-113 standard", + ); } -} \ No newline at end of file +} diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 616b0a1..c28cfb6 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -2,17 +2,16 @@ import { Web3Sdk } from ".."; import { MultiChainWalletInfo, MultiChainWalletInstance, - SupportedChain} from "../../types/core/multi-chain"; + SupportedChain, +} from "../../types/core/multi-chain"; import { Web3ProjectCardanoWallet, Web3ProjectSparkWallet } from "../../types"; import { CardanoWalletDeveloperControlled } from "./cardano"; -import { SparkWalletDeveloperControlled } from "./spark"; +import { SparkIssuerWalletDeveloperControlled } from "./spark-issuer"; import { MeshWallet } from "@meshsdk/wallet"; import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; import { deserializeBech32Address } from "@meshsdk/core-cst"; import { encryptWithPublicKey, decryptWithPrivateKey } from "../../functions"; import { v4 as uuidv4 } from "uuid"; -export { CardanoWalletDeveloperControlled } from "./cardano"; -export { SparkWalletDeveloperControlled } from "./spark"; /** * The `WalletDeveloperControlled` class provides functionality for managing developer-controlled wallets @@ -45,12 +44,12 @@ export { SparkWalletDeveloperControlled } from "./spark"; export class WalletDeveloperControlled { readonly sdk: Web3Sdk; cardano: CardanoWalletDeveloperControlled; - spark: SparkWalletDeveloperControlled; + sparkIssuer: SparkIssuerWalletDeveloperControlled; constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; this.cardano = new CardanoWalletDeveloperControlled({ sdk }); - this.spark = new SparkWalletDeveloperControlled({ sdk }); + this.sparkIssuer = new SparkIssuerWalletDeveloperControlled({ sdk }); } /** @@ -62,13 +61,13 @@ export class WalletDeveloperControlled { * * @example * ```typescript - * const { sparkWallet, cardanoWallet } = await sdk.wallet.createWallet({ + * const { sparkIssuerWallet, cardanoWallet } = await sdk.wallet.createWallet({ * tags: ["tokenization"], * networkId: 1 // mainnet * }); * * // Both wallets share the same mnemonic - * await sparkWallet.createToken({ + * await sparkIssuerWallet.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, @@ -76,11 +75,13 @@ export class WalletDeveloperControlled { * }); * ``` */ - async createWallet(options: { - tags?: string[]; - } = {}): Promise<{ + async createWallet( + options: { + tags?: string[]; + } = {}, + ): Promise<{ info: MultiChainWalletInfo; - sparkWallet: SparkWalletDeveloperControlled; + sparkIssuerWallet: SparkIssuerWalletDeveloperControlled; cardanoWallet: MeshWallet; }> { const project = await this.sdk.getProject(); @@ -104,26 +105,30 @@ export class WalletDeveloperControlled { }); await cardanoWallet.init(); - const [{ wallet: sparkMainnetWallet }, { wallet: sparkRegtestWallet }] = await Promise.all([ - IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic.join(" "), - options: { network: "MAINNET" }, - }), - IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic.join(" "), - options: { network: "REGTEST" }, - }), - ]); + const [{ wallet: sparkMainnetWallet }, { wallet: sparkRegtestWallet }] = + await Promise.all([ + IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic.join(" "), + options: { network: "MAINNET" }, + }), + IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic.join(" "), + options: { network: "REGTEST" }, + }), + ]); const addresses = cardanoWallet.getAddresses(); - const { pubKeyHash, stakeCredentialHash } = deserializeBech32Address(addresses.baseAddressBech32!); + const { pubKeyHash, stakeCredentialHash } = deserializeBech32Address( + addresses.baseAddressBech32!, + ); const [mainnetPublicKey, regtestPublicKey] = await Promise.all([ sparkMainnetWallet.getIdentityPublicKey(), sparkRegtestWallet.getIdentityPublicKey(), ]); const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; - const sparkWallet = networkId === 1 ? sparkMainnetWallet : sparkRegtestWallet; + const sparkWallet = + networkId === 1 ? sparkMainnetWallet : sparkRegtestWallet; const walletData = { id: walletId, @@ -133,14 +138,14 @@ export class WalletDeveloperControlled { networkId, chains: { cardano: { pubKeyHash, stakeCredentialHash }, - spark: { mainnetPublicKey, regtestPublicKey } + spark: { mainnetPublicKey, regtestPublicKey }, }, createdAt: new Date().toISOString(), }; const { status } = await this.sdk.axiosInstance.post( `api/project-wallet`, - walletData + walletData, ); if (status === 200) { @@ -162,25 +167,25 @@ export class WalletDeveloperControlled { stakeCredentialHash, }; - const sparkWalletDev = new SparkWalletDeveloperControlled({ + const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, wallet: sparkWallet, - walletInfo: sparkWalletInfo + walletInfo: sparkWalletInfo, }); - + const cardanoWalletDev = new CardanoWalletDeveloperControlled({ sdk: this.sdk, wallet: cardanoWallet, - walletInfo: cardanoWalletInfo + walletInfo: cardanoWalletInfo, }); - this.spark = sparkWalletDev; + this.sparkIssuer = sparkWalletDev; this.cardano = cardanoWalletDev; return { info: walletData as MultiChainWalletInfo, - sparkWallet: sparkWalletDev, - cardanoWallet: cardanoWallet + sparkIssuerWallet: sparkWalletDev, + cardanoWallet: cardanoWallet, }; } @@ -196,21 +201,21 @@ export class WalletDeveloperControlled { * @example * ```typescript * const { sparkWallet, cardanoWallet } = await sdk.wallet.initWallet("wallet-id"); - * + * * // Use either wallet directly * await sparkWallet.mintTokens(BigInt("1000000")); * await cardanoWallet.sendAssets({...}); * ``` */ - async initWallet( - walletId: string - ): Promise<{ + async initWallet(walletId: string): Promise<{ info: MultiChainWalletInfo; - sparkWallet: SparkWalletDeveloperControlled; + sparkWallet: SparkIssuerWalletDeveloperControlled; cardanoWallet: MeshWallet; }> { if (!this.sdk.privateKey) { - throw new Error("Private key required to load developer-controlled wallet"); + throw new Error( + "Private key required to load developer-controlled wallet", + ); } const walletInfo = await this.getProjectWallet(walletId); @@ -249,28 +254,29 @@ export class WalletDeveloperControlled { tags: walletInfo.tags || [], key: walletInfo.key, pubKeyHash: walletInfo.chains?.cardano?.pubKeyHash || "", - stakeCredentialHash: walletInfo.chains?.cardano?.stakeCredentialHash || "", + stakeCredentialHash: + walletInfo.chains?.cardano?.stakeCredentialHash || "", }; - const sparkWalletDev = new SparkWalletDeveloperControlled({ + const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, wallet: sparkWallet, - walletInfo: sparkWalletInfo + walletInfo: sparkWalletInfo, }); - + const cardanoWalletDev = new CardanoWalletDeveloperControlled({ sdk: this.sdk, wallet: cardanoWallet, - walletInfo: cardanoWalletInfo + walletInfo: cardanoWalletInfo, }); - this.spark = sparkWalletDev; + this.sparkIssuer = sparkWalletDev; this.cardano = cardanoWalletDev; return { info: walletInfo, sparkWallet: sparkWalletDev, - cardanoWallet: cardanoWallet + cardanoWallet: cardanoWallet, }; } @@ -297,7 +303,7 @@ export class WalletDeveloperControlled { const walletInfo = await this.getProjectWallet(projectWalletId); const instance: MultiChainWalletInstance = { - info: walletInfo + info: walletInfo, }; let mnemonic: string | null = null; @@ -309,8 +315,12 @@ export class WalletDeveloperControlled { } const networkId = this.sdk.network === "mainnet" ? 1 : 0; - - if ((chain === "cardano" || !chain) && walletInfo.chains.cardano && mnemonic) { + + if ( + (chain === "cardano" || !chain) && + walletInfo.chains.cardano && + mnemonic + ) { const cardanoWallet = new MeshWallet({ networkId: networkId, key: { type: "mnemonic", words: mnemonic.split(" ") }, @@ -338,10 +348,10 @@ export class WalletDeveloperControlled { network: sparkNetwork, }; - instance.sparkWallet = new SparkWalletDeveloperControlled({ + instance.sparkIssuerWallet = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, wallet: sparkWallet, - walletInfo: sparkWalletInfo + walletInfo: sparkWalletInfo, }); } @@ -353,7 +363,7 @@ export class WalletDeveloperControlled { */ async getProjectWallet(walletId: string): Promise { const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}/${walletId}` + `api/project-wallet/${this.sdk.projectId}/${walletId}`, ); if (status === 200) { @@ -368,7 +378,7 @@ export class WalletDeveloperControlled { */ async getProjectWallets(): Promise { const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}` + `api/project-wallet/${this.sdk.projectId}`, ); if (status === 200) { @@ -378,3 +388,6 @@ export class WalletDeveloperControlled { throw new Error("Failed to get project wallets"); } } + +export { CardanoWalletDeveloperControlled } from "./cardano"; +export { SparkIssuerWalletDeveloperControlled } from "./spark-issuer"; diff --git a/src/sdk/wallet-developer-controlled/spark.ts b/src/sdk/wallet-developer-controlled/spark-issuer.ts similarity index 98% rename from src/sdk/wallet-developer-controlled/spark.ts rename to src/sdk/wallet-developer-controlled/spark-issuer.ts index b0d3c8f..d5e79f0 100644 --- a/src/sdk/wallet-developer-controlled/spark.ts +++ b/src/sdk/wallet-developer-controlled/spark-issuer.ts @@ -36,7 +36,7 @@ import { * const balance = await sparkWallet.getTokenBalance(); * ``` */ -export class SparkWalletDeveloperControlled { +export class SparkIssuerWalletDeveloperControlled { readonly sdk: Web3Sdk; private wallet: IssuerSparkWallet | null = null; private walletInfo: Web3ProjectSparkWallet | null = null; @@ -376,7 +376,7 @@ export class SparkWalletDeveloperControlled { * await sparkWallet.mintTokens(BigInt("1000000")); * ``` */ - async getWallet(walletId: string): Promise { + async getWallet(walletId: string): Promise { if (this.sdk.privateKey === undefined) { throw new Error("Private key not found - required to decrypt wallet"); } @@ -399,7 +399,7 @@ export class SparkWalletDeveloperControlled { options: { network: sparkWalletInfo.network }, }); - return new SparkWalletDeveloperControlled({ + return new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, wallet: issuerSparkWallet, walletInfo: sparkWalletInfo diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index 3779cc3..1d94727 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -1,5 +1,5 @@ import { MeshWallet } from "@meshsdk/wallet"; -import type { SparkWalletDeveloperControlled } from "../../sdk/wallet-developer-controlled/spark"; +import type { SparkIssuerWalletDeveloperControlled } from "../../sdk/wallet-developer-controlled/spark-issuer"; /** * Standardized network ID type (0 = testnet, 1 = mainnet) @@ -42,7 +42,7 @@ export interface MultiChainWalletInfo { export interface MultiChainWalletInstance { info: MultiChainWalletInfo; cardanoWallet?: MeshWallet; - sparkWallet?: SparkWalletDeveloperControlled; + sparkIssuerWallet?: SparkIssuerWalletDeveloperControlled; } /** From a2e83a82b5ec165172b0d3a1be91d8a0125ce513 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Thu, 27 Nov 2025 06:44:17 +0800 Subject: [PATCH 14/30] feat: add enableTokenization option for wallet-token creation flow --- src/index.ts | 2 + src/sdk/tokenization/spark.ts | 638 +++++++++++++++++- .../wallet-developer-controlled/cardano.ts | 8 - src/sdk/wallet-developer-controlled/index.ts | 31 +- .../spark-issuer.ts | 397 +---------- src/types/spark/index.ts | 2 +- src/types/spark/tokenization.ts | 139 ++++ 7 files changed, 786 insertions(+), 431 deletions(-) create mode 100644 src/types/spark/tokenization.ts diff --git a/src/index.ts b/src/index.ts index 4b36328..6f989d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ export { getNetworkFromSparkAddress, type Bech32mTokenIdentifier, encodeBech32mTokenIdentifier, + decodeBech32mTokenIdentifier, + getNetworkFromBech32mTokenIdentifier, SparkWallet, } from "@buildonspark/spark-sdk"; diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index 0580927..60e6cca 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -1,53 +1,641 @@ import { Web3Sdk } from ".."; +import { decryptWithPrivateKey } from "../../functions"; +import { Web3ProjectSparkWallet } from "../../types"; +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import { + Bech32mTokenIdentifier, + decodeBech32mTokenIdentifier, +} from "@buildonspark/spark-sdk"; +import { extractIdentityPublicKey } from "../../chains/spark/utils"; +import { SparkFreezeResult } from "../../types/spark/dev-wallet"; +import { + TokenizationTransaction, + TokenizationFrozenAddress, + TokenizationPaginationInfo, + TokenizationPolicy, + CreateTokenParams, + MintTokensParams, + TransferTokensParams, + BurnTokensParams, + FreezeTokensParams, + UnfreezeTokensParams, + ListTransactionsParams, + ListFrozenAddressesParams, + ListTokenizationPoliciesParams, +} from "../../types/spark/tokenization"; + +export type { + CreateTokenParams, + MintTokensParams, + TransferTokensParams, + BurnTokensParams, + FreezeTokensParams, + UnfreezeTokensParams, + ListTransactionsParams, + ListFrozenAddressesParams, + ListTokenizationPoliciesParams, + TokenizationTransaction, + TokenizationFrozenAddress, + TokenizationPaginationInfo, + TokenizationPolicy, +}; /** - * TODO: - * - Replace with actual implementation - * - provide correct inputs - * - define output types for all methods - * - integrate API request if need database calls, e.g. get token policies from database + * The `TokenizationSpark` class provides methods for token operations on Spark network. + * + * @example + * ```typescript + * const sdk = new Web3Sdk({ ... }); + * + * // Create a new token (automatically creates a new wallet) + * const result = await sdk.tokenization.spark.createToken({ + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * isFreezable: true, + * }); + * // result contains { txId, tokenId, walletId } + * + * // Load an existing token by ID (initializes wallet from policy) + * await sdk.tokenization.spark.initWalletByTokenId("token-id-hex"); + * + * // Perform operations on the loaded token + * await sdk.tokenization.spark.mintTokens({ amount: BigInt("1000000") }); + * const balance = await sdk.tokenization.spark.getTokenBalance(); + * ``` */ - export class TokenizationSpark { private readonly sdk: Web3Sdk; + private wallet: IssuerSparkWallet | null = null; + private walletInfo: Web3ProjectSparkWallet | null = null; constructor({ sdk }: { sdk: Web3Sdk }) { - { - this.sdk = sdk; + this.sdk = sdk; + } + + /** + * Sets the wallet for tokenization operations. + * @internal Called by sdk.wallet.createWallet() when enableTokenization is true. + */ + setWallet(wallet: IssuerSparkWallet, walletInfo: Web3ProjectSparkWallet): void { + this.wallet = wallet; + this.walletInfo = walletInfo; + } + + /** + * Gets the current wallet ID if one is loaded. + */ + getWalletId(): string | null { + return this.walletInfo?.id ?? null; + } + + /** + * Internal method to initialize the wallet by wallet ID. + */ + private async initWalletByWalletId(walletId: string): Promise { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found - required to decrypt wallet"); + } + + const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, + ); + + if (status !== 200) { + throw new Error("Failed to get Spark wallet"); + } + + const walletInfo = data as Web3ProjectSparkWallet; + + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: walletInfo.key, + }); + + const { wallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic, + options: { network: walletInfo.network }, + }); + + this.wallet = wallet; + this.walletInfo = walletInfo; + } + + /** + * Initializes the tokenization instance with a token by token ID. + * Looks up the tokenization policy to get the wallet and initializes it. + * Must be called before performing token operations on existing tokens. + * + * @param tokenId - The token ID (hex or bech32m format, e.g., "abc123..." or "btknrt1...") + * @returns The tokenization policy + * + * @example + * ```typescript + * // Using hex format + * const policy = await sdk.tokenization.spark.initWallet("abc123..."); + * // Or using bech32m format + * const policy = await sdk.tokenization.spark.initWallet("btknrt1..."); + * const metadata = await sdk.tokenization.spark.getTokenMetadata(); + * await sdk.tokenization.spark.mintTokens({ amount: BigInt(1000) }); + * ``` + */ + async initWallet(tokenId: string): Promise { + const normalizedTokenId = this.normalizeTokenId(tokenId); + const policy = await this.getTokenizationPolicy(normalizedTokenId); + await this.initWalletByWalletId(policy.walletId); + return policy; + } + + /** + * Normalizes token ID to hex format. + * If bech32m encoded (btkn1.., btknrt1.., etc), decodes to hex. Otherwise returns as-is. + */ + private normalizeTokenId(tokenId: string): string { + const network = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; + try { + const decoded = decodeBech32mTokenIdentifier(tokenId as Bech32mTokenIdentifier, network); + return Buffer.from(decoded.tokenIdentifier).toString("hex"); + } catch { + return tokenId; + } + } + + /** + * Creates a new token on the Spark network. + * Requires enableTokenization: true in createWallet() to be called first. + * + * @param params - Token creation parameters + * @returns Object containing txId, tokenId, and walletId + * + * @example + * ```typescript + * // Create wallet with tokenization enabled + * const { info } = await sdk.wallet.createWallet({ + * tags: ["tokenization"], + * enableTokenization: true + * }); + * + * // Create token - wallet is already linked + * const result = await sdk.tokenization.spark.createToken({ + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * isFreezable: true, + * }); + * ``` + */ + async createToken(params: CreateTokenParams): Promise<{ + txId: string; + tokenId: string; + walletId: string; + }> { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Use createWallet({ enableTokenization: true }) first."); } + + const txId = await this.wallet.createToken({ + tokenName: params.tokenName, + tokenTicker: params.tokenTicker, + decimals: params.decimals, + maxSupply: params.maxSupply, + isFreezable: params.isFreezable, + }); + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + // Save tokenization policy to database + try { + await this.sdk.axiosInstance.post("/api/tokenization/tokens", { + tokenId, + projectId: this.sdk.projectId, + walletId: this.walletInfo.id, + chain: "spark", + network: this.walletInfo.network.toLowerCase(), + }); + + // Log the create transaction + await this.logTransaction({ + tokenId, + walletInfo: this.walletInfo, + type: "create", + txHash: txId, + metadata: { + tokenName: params.tokenName, + tokenTicker: params.tokenTicker, + decimals: params.decimals, + maxSupply: params.maxSupply?.toString(), + isFreezable: params.isFreezable, + }, + }); + } catch (saveError) { + console.warn("Failed to save token to database:", saveError); + } + + return { txId, tokenId, walletId: this.walletInfo.id }; + } + + /** + * Mints tokens from the issuer wallet. + * Requires initWallet() to be called first. + * + * @param params - Mint parameters including amount + * @returns Transaction ID of the mint operation + */ + async mintTokens(params: MintTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const txHash = await this.wallet.mintTokens(params.amount); + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + await this.logTransaction({ + tokenId, + walletInfo: this.walletInfo, + type: "mint", + txHash, + amount: params.amount.toString(), + }); + + return txHash; + } + + /** + * Gets the token balance for an issuer wallet. + * Requires initWallet() to be called first. + * + * @returns Balance information + */ + async getTokenBalance(): Promise<{ balance: string }> { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + const result = await this.wallet.getIssuerTokenBalance(); + return { balance: result.balance.toString() }; + } + + /** + * Gets metadata for the token created by an issuer wallet. + * Requires initWallet() to be called first. + * + * @returns Token metadata + */ + async getTokenMetadata() { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + return await this.wallet.getIssuerTokenMetadata(); + } + + /** + * Transfers tokens from the issuer wallet to another address. + * Requires initWallet() to be called first. + * + * @param params - Transfer parameters + * @returns Transaction ID of the transfer + */ + async transferTokens(params: TransferTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const txHash = await this.wallet.transferTokens({ + tokenIdentifier: tokenId as Bech32mTokenIdentifier, + tokenAmount: params.amount, + receiverSparkAddress: params.toAddress, + }); + + const issuerAddress = await this.wallet.getSparkAddress(); + await this.logTransaction({ + tokenId, + walletInfo: this.walletInfo, + type: "transfer", + txHash, + amount: params.amount.toString(), + fromAddress: issuerAddress, + toAddress: params.toAddress, + }); + + return txHash; } - async createToken({}): Promise { - return ""; + /** + * Burns tokens permanently from circulation. + * Requires initWallet() to be called first. + * + * @param params - Burn parameters + * @returns Transaction ID of the burn operation + */ + async burnTokens(params: BurnTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const txHash = await this.wallet.burnTokens(params.amount); + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + await this.logTransaction({ + tokenId, + walletInfo: this.walletInfo, + type: "burn", + txHash, + amount: params.amount.toString(), + }); + + return txHash; } - async mintTokens({}): Promise { - return ""; + /** + * Freezes tokens at a specific Spark address for compliance purposes. + * Requires initWallet() to be called first. + * + * @param params - Freeze parameters + * @returns Freeze operation results + */ + async freezeTokens(params: FreezeTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const result = await this.wallet.freezeTokens(params.address); + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + try { + const publicKeyHash = extractIdentityPublicKey(params.address); + + if (!publicKeyHash) { + throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); + } + + // Update frozen addresses table + await this.sdk.axiosInstance.post("/api/tokenization/frozen-addresses", { + tokenId, + projectId: this.sdk.projectId, + projectWalletId: this.walletInfo.id, + chain: "spark", + network: this.walletInfo.network.toLowerCase(), + publicKeyHash, + isFrozen: true, + freezeReason: params.freezeReason || "Frozen by issuer", + frozenAt: new Date().toISOString(), + }); + + // Log the freeze transaction + await this.logTransaction({ + tokenId, + walletInfo: this.walletInfo, + type: "freeze", + toAddress: params.address, + amount: result.impactedTokenAmount.toString(), + metadata: { + freezeReason: params.freezeReason, + impactedOutputIds: result.impactedOutputIds, + publicKeyHash, + }, + }); + + } catch (saveError) { + console.warn("Failed to save freeze operation:", saveError); + } + + return { + impactedOutputIds: result.impactedOutputIds, + impactedTokenAmount: result.impactedTokenAmount.toString(), + }; } - async getTokenBalance(address?: string) {} + /** + * Unfreezes tokens at a specific Spark address. + * Requires initWallet() to be called first. + * + * @param params - Unfreeze parameters + * @returns Unfreeze operation results + */ + async unfreezeTokens(params: UnfreezeTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const result = await this.wallet.unfreezeTokens(params.address); + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + try { + const publicKeyHash = extractIdentityPublicKey(params.address); + + if (!publicKeyHash) { + throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); + } + + // Update frozen addresses table + await this.sdk.axiosInstance.put("/api/tokenization/frozen-addresses", { + tokenId, + publicKeyHash, + projectId: this.sdk.projectId, + projectWalletId: this.walletInfo.id, + }); + + // Log the unfreeze transaction + await this.logTransaction({ + tokenId, + walletInfo: this.walletInfo, + type: "unfreeze", + toAddress: params.address, + amount: result.impactedTokenAmount.toString(), + metadata: { + impactedOutputIds: result.impactedOutputIds, + publicKeyHash, + }, + }); + } catch (saveError) { + console.warn("Failed to save unfreeze operation:", saveError); + } - async getTokenMetadata(): Promise<{}> { - return {}; + return { + impactedOutputIds: result.impactedOutputIds, + impactedTokenAmount: result.impactedTokenAmount.toString(), + }; } - async transferTokens({}): Promise { - return ""; + /** + * Lists frozen addresses for a token from the database. + * Requires initWallet() to be called first. + * + * @param params - Query parameters including pagination + * @returns List of frozen addresses with pagination info + */ + async getFrozenAddresses(params?: ListFrozenAddressesParams): Promise<{ + frozenAddresses: TokenizationFrozenAddress[]; + pagination: TokenizationPaginationInfo; + }> { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const { includeUnfrozen = false, page = 1, limit = 15 } = params || {}; + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + const { data, status } = await this.sdk.axiosInstance.get( + `api/tokenization/frozen-addresses`, + { + params: { + tokenId, + projectId: this.sdk.projectId, + includeUnfrozen, + page, + limit, + }, + } + ); + + if (status === 200) { + return data as { + frozenAddresses: TokenizationFrozenAddress[]; + pagination: TokenizationPaginationInfo; + }; + } + + throw new Error("Failed to get frozen addresses"); } - async batchTransferTokens({}): Promise<{}> { - return {}; + /** + * Lists token transactions from the database. + * Requires initWallet() to be called first. + * + * @param params - Query parameters including type filter and pagination + * @returns List of transactions with pagination info + */ + async getTransactions(params?: ListTransactionsParams): Promise<{ + transactions: TokenizationTransaction[]; + pagination: TokenizationPaginationInfo; + }> { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(walletId) first."); + } + + const { type, page = 1, limit = 50 } = params || {}; + + const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + + const { data, status } = await this.sdk.axiosInstance.get( + `api/tokenization/transactions`, + { + params: { + tokenId, + projectId: this.sdk.projectId, + ...(type && { type }), + page, + limit, + }, + } + ); + + if (status === 200) { + return data as { + transactions: TokenizationTransaction[]; + pagination: TokenizationPaginationInfo; + }; + } + + throw new Error("Failed to get transactions"); } - async freezeTokens(address: string): Promise<{}> { - return {}; + /** + * Lists tokenization policies for the current project from the database. + * + * @param params - Optional filter and pagination parameters + * @returns List of tokenization policies with pagination info + */ + async getTokenizationPolicies(params?: ListTokenizationPoliciesParams): Promise<{ + tokens: TokenizationPolicy[]; + pagination: TokenizationPaginationInfo; + }> { + const { tokenId, page = 1, limit = 15 } = params || {}; + + const { data, status } = await this.sdk.axiosInstance.get( + `api/tokenization/policies`, + { + params: { + projectId: this.sdk.projectId, + ...(tokenId && { tokenId }), + page, + limit, + }, + } + ); + + if (status === 200) { + return data as { + tokens: TokenizationPolicy[]; + pagination: TokenizationPaginationInfo; + }; + } + + throw new Error("Failed to get tokenization policies"); } - async unfreezeTokens(address: string): Promise<{}> { - return {}; + /** + * Gets a single tokenization policy by token ID. + * + * @param tokenId - The token ID to look up + * @returns The tokenization policy + */ + async getTokenizationPolicy(tokenId: string): Promise { + const { tokens } = await this.getTokenizationPolicies({ tokenId, limit: 1 }); + const policy = tokens[0]; + + if (!policy) { + throw new Error("Tokenization policy not found"); + } + + return policy; } - async burnTokens(quantity: number): Promise { - return ""; + /** + * Internal helper to log token transactions to the database + */ + private async logTransaction(params: { + tokenId: string; + walletInfo: Web3ProjectSparkWallet; + type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + txHash?: string; + amount?: string; + fromAddress?: string; + toAddress?: string; + status?: string; + metadata?: Record; + }): Promise { + try { + await this.sdk.axiosInstance.post("/api/tokenization/transactions", { + tokenId: params.tokenId, + projectId: this.sdk.projectId, + projectWalletId: params.walletInfo.id, + type: params.type, + chain: "spark", + network: params.walletInfo.network.toLowerCase(), + txHash: params.txHash, + amount: params.amount, + fromAddress: params.fromAddress, + toAddress: params.toAddress, + status: params.status || "success", + metadata: params.metadata, + }); + } catch (error) { + console.warn(`Failed to log ${params.type} transaction:`, error); + } } } diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index 0606a94..acb69f5 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -19,21 +19,13 @@ import { */ export class CardanoWalletDeveloperControlled { readonly sdk: Web3Sdk; - private wallet: MeshWallet | null = null; - private walletInfo: Web3ProjectCardanoWallet | null = null; constructor({ sdk, - wallet, - walletInfo, }: { sdk: Web3Sdk; - wallet?: MeshWallet; - walletInfo?: Web3ProjectCardanoWallet; }) { this.sdk = sdk; - this.wallet = wallet || null; - this.walletInfo = walletInfo || null; } /** diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index c28cfb6..bf75c7f 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -56,18 +56,20 @@ export class WalletDeveloperControlled { * Creates a new developer-controlled wallet with both Spark and Cardano chains using shared mnemonic. * * @param options - Wallet creation options - * @param options.networkId - Network ID (0 = testnet, 1 = mainnet) + * @param options.tags - Optional tags for the wallet + * @param options.enableTokenization - If true, links the wallet to sdk.tokenization.spark for seamless token creation * @returns Promise that resolves to both chain wallet instances * * @example * ```typescript - * const { sparkIssuerWallet, cardanoWallet } = await sdk.wallet.createWallet({ + * // With tokenization enabled + * const { info } = await sdk.wallet.createWallet({ * tags: ["tokenization"], - * networkId: 1 // mainnet + * enableTokenization: true * }); * - * // Both wallets share the same mnemonic - * await sparkIssuerWallet.createToken({ + * // createToken works seamlessly - wallet is already linked + * await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, @@ -78,6 +80,7 @@ export class WalletDeveloperControlled { async createWallet( options: { tags?: string[]; + enableTokenization?: boolean; } = {}, ): Promise<{ info: MultiChainWalletInfo; @@ -169,19 +172,19 @@ export class WalletDeveloperControlled { const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, - wallet: sparkWallet, - walletInfo: sparkWalletInfo, }); const cardanoWalletDev = new CardanoWalletDeveloperControlled({ - sdk: this.sdk, - wallet: cardanoWallet, - walletInfo: cardanoWalletInfo, + sdk: this.sdk }); this.sparkIssuer = sparkWalletDev; this.cardano = cardanoWalletDev; + if (options.enableTokenization) { + this.sdk.tokenization.spark.setWallet(sparkWallet, sparkWalletInfo); + } + return { info: walletData as MultiChainWalletInfo, sparkIssuerWallet: sparkWalletDev, @@ -260,14 +263,10 @@ export class WalletDeveloperControlled { const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, - wallet: sparkWallet, - walletInfo: sparkWalletInfo, }); const cardanoWalletDev = new CardanoWalletDeveloperControlled({ - sdk: this.sdk, - wallet: cardanoWallet, - walletInfo: cardanoWalletInfo, + sdk: this.sdk }); this.sparkIssuer = sparkWalletDev; @@ -350,8 +349,6 @@ export class WalletDeveloperControlled { instance.sparkIssuerWallet = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, - wallet: sparkWallet, - walletInfo: sparkWalletInfo, }); } diff --git a/src/sdk/wallet-developer-controlled/spark-issuer.ts b/src/sdk/wallet-developer-controlled/spark-issuer.ts index d5e79f0..4a09283 100644 --- a/src/sdk/wallet-developer-controlled/spark-issuer.ts +++ b/src/sdk/wallet-developer-controlled/spark-issuer.ts @@ -1,409 +1,50 @@ import { Web3Sdk } from ".."; -import { decryptWithPrivateKey } from "../../functions"; import { Web3ProjectSparkWallet } from "../../types"; -import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; -import { Bech32mTokenIdentifier } from "@buildonspark/spark-sdk"; -import { extractIdentityPublicKey } from "../../chains/spark/utils"; -import { - SparkFreezeTokensParams, - SparkUnfreezeTokensParams, - SparkFreezeResult -} from "../../types/spark/dev-wallet"; /** - * SparkWalletDeveloperControlled - Developer-controlled Spark wallet for token operations + * SparkIssuerWalletDeveloperControlled - Developer-controlled Spark issuer wallet management * - * Provides token issuance, minting, transfer, and compliance operations on the Spark network. - * Wraps an IssuerSparkWallet instance with database synchronization. + * Provides wallet management operations for Spark issuer wallets. + * Token operations (create, mint, transfer, burn, freeze) are handled by TokenizationSpark. * * @example * ```typescript - * // Create wallet via SDK - * const { sparkWallet } = await sdk.wallet.createWallet({ - * tags: ["tokenization"] - * }); + * // List all Spark wallets + * const wallets = await sdk.wallet.sparkIssuer.list(); * - * // Token operations - * await sparkWallet.createToken({ - * tokenName: "MyToken", - * tokenTicker: "MTK", - * decimals: 8, - * maxSupply: "1000000", - * isFreezable: true - * }); + * // Get wallets by tag + * const tokenizationWallets = await sdk.wallet.sparkIssuer.getByTag("tokenization"); * - * await sparkWallet.mintTokens(BigInt("1000000")); - * const balance = await sparkWallet.getTokenBalance(); + * // Get wallet info by ID + * const walletInfo = await sdk.wallet.sparkIssuer.get("wallet-id"); * ``` */ export class SparkIssuerWalletDeveloperControlled { readonly sdk: Web3Sdk; - private wallet: IssuerSparkWallet | null = null; - private walletInfo: Web3ProjectSparkWallet | null = null; - constructor({ sdk, wallet, walletInfo }: { - sdk: Web3Sdk; - wallet?: IssuerSparkWallet; - walletInfo?: Web3ProjectSparkWallet; - }) { + constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; - this.wallet = wallet || null; - this.walletInfo = walletInfo || null; } /** - * Internal method to ensure wallet is loaded - */ - private ensureWallet(): IssuerSparkWallet { - if (!this.wallet) { - throw new Error("Wallet not initialized. Use sdk.wallet.createWallet() or sdk.wallet.initWallet() first."); - } - return this.wallet; - } - - /** - * Internal helper to log token transactions to the database - */ - private async logTransaction(params: { - tokenId: string; - type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; - txHash?: string; - amount?: string; - fromAddress?: string; - toAddress?: string; - status?: string; - metadata?: Record; - }): Promise { - try { - await this.sdk.axiosInstance.post("/api/tokenization/transactions", { - tokenId: params.tokenId, - projectId: this.sdk.projectId, - projectWalletId: this.walletInfo?.id, - type: params.type, - chain: "spark", - network: this.walletInfo?.network.toLowerCase(), - txHash: params.txHash, - amount: params.amount, - fromAddress: params.fromAddress, - toAddress: params.toAddress, - status: params.status || "success", - metadata: params.metadata, - }); - } catch (error) { - console.warn(`Failed to log ${params.type} transaction:`, error); - } - } - - /** - * Internal helper to get the token ID (hex) from wallet metadata - */ - private async getTokenIdHex(): Promise { - const wallet = this.ensureWallet(); - const tokenMetadata = await wallet.getIssuerTokenMetadata(); - return Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); - } - - /** - * Creates a new token on the Spark network using this wallet as the issuer. - * - * @param params Token creation parameters (matches IssuerSparkWallet.createToken) - * @returns Promise resolving to the transaction ID - * @throws Error if wallet is not initialized or token creation fails - */ - async createToken(params: { - tokenName: string; - tokenTicker: string; - decimals: number; - maxSupply?: bigint; - isFreezable: boolean; - }): Promise { - const wallet = this.ensureWallet(); - const transactionId = await wallet.createToken(params); - - try { - const tokenMetadata = await wallet.getIssuerTokenMetadata(); - const rawTokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); - - // Create tokenization policy - await this.sdk.axiosInstance.post("/api/tokenization/tokens", { - tokenId: rawTokenIdHex, - projectId: this.sdk.projectId, - walletId: this.walletInfo?.id, - chain: "spark", - network: this.walletInfo?.network.toLowerCase(), - }); - - // Log the create transaction - await this.logTransaction({ - tokenId: rawTokenIdHex, - type: "create", - txHash: transactionId, - metadata: { - tokenName: params.tokenName, - tokenTicker: params.tokenTicker, - decimals: params.decimals, - maxSupply: params.maxSupply?.toString(), - isFreezable: params.isFreezable, - }, - }); - } catch (saveError) { - console.warn("Failed to save token to main app:", saveError); - } - - return transactionId; - } - - /** - * Mints tokens from this issuer wallet. - * - * @param amount Amount of tokens to mint (as bigint) - * @returns Promise resolving to the transaction ID - * @throws Error if wallet is not initialized or minting fails - */ - async mintTokens(amount: bigint): Promise { - const wallet = this.ensureWallet(); - const txHash = await wallet.mintTokens(amount); - - // Log the mint transaction - const tokenId = await this.getTokenIdHex(); - await this.logTransaction({ - tokenId, - type: "mint", - txHash, - amount: amount.toString(), - }); - - return txHash; - } - - /** - * Transfers tokens from this wallet to another Spark address. - * - * @param params Transfer parameters including token identifier, amount, and destination - * @returns Promise resolving to the transaction ID - * @throws Error if wallet is not initialized or transfer fails - */ - async transferTokens(params: { - tokenIdentifier: string; - amount: bigint; - toAddress: string; - }): Promise { - const wallet = this.ensureWallet(); - const txHash = await wallet.transferTokens({ - tokenIdentifier: params.tokenIdentifier as Bech32mTokenIdentifier, - tokenAmount: params.amount, - receiverSparkAddress: params.toAddress, - }); - - // Log the transfer transaction - const tokenId = await this.getTokenIdHex(); - const issuerAddress = await wallet.getSparkAddress(); - await this.logTransaction({ - tokenId, - type: "transfer", - txHash, - amount: params.amount.toString(), - fromAddress: issuerAddress, - toAddress: params.toAddress, - }); - - return txHash; - } - - /** - * Burns tokens permanently from circulation. - * - * @param amount Amount of tokens to burn (as bigint) - * @returns Promise resolving to the transaction ID - * @throws Error if wallet is not initialized or burning fails - */ - async burnTokens(amount: bigint): Promise { - const wallet = this.ensureWallet(); - const txHash = await wallet.burnTokens(amount); - - // Log the burn transaction - const tokenId = await this.getTokenIdHex(); - await this.logTransaction({ - tokenId, - type: "burn", - txHash, - amount: amount.toString(), - }); - - return txHash; - } - - /** - * Gets the token balance for this issuer wallet. - * - * @returns Promise resolving to balance information - * @throws Error if wallet is not initialized - */ - async getTokenBalance() { - const wallet = this.ensureWallet(); - const result = await wallet.getIssuerTokenBalance(); - return { balance: result.balance.toString() }; - } - - /** - * Gets metadata for tokens created by this issuer wallet. - * - * @returns Promise resolving to token metadata - * @throws Error if wallet is not initialized - */ - async getTokenMetadata() { - const wallet = this.ensureWallet(); - return await wallet.getIssuerTokenMetadata(); - } - - /** - * Freezes tokens at a specific Spark address for compliance purposes. - * - * @param params Freeze parameters including address and optional reason - * @returns Promise resolving to freeze operation results - * @throws Error if wallet is not initialized or freeze operation fails - */ - async freezeTokens(params: SparkFreezeTokensParams): Promise { - const wallet = this.ensureWallet(); - const result = await wallet.freezeTokens(params.address); - - // Save freeze operation to database - try { - const tokenMetadata = await wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); - const publicKeyHash = extractIdentityPublicKey(params.address); - - if (!publicKeyHash) { - throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); - } - - // Update frozen addresses table - await this.sdk.axiosInstance.post("/api/tokenization/frozen-addresses", { - tokenId, - projectId: this.sdk.projectId, - projectWalletId: this.walletInfo?.id, - chain: "spark", - network: this.walletInfo?.network.toLowerCase(), - publicKeyHash, - isFrozen: true, - freezeReason: params.freezeReason || "Frozen by issuer", - frozenAt: new Date().toISOString(), - }); - - // Log the freeze transaction - await this.logTransaction({ - tokenId, - type: "freeze", - toAddress: params.address, - amount: result.impactedTokenAmount.toString(), - metadata: { - freezeReason: params.freezeReason, - impactedOutputIds: result.impactedOutputIds, - publicKeyHash, - }, - }); - } catch (saveError) { - console.warn("Failed to save freeze operation:", saveError); - } - - return { - impactedOutputIds: result.impactedOutputIds, - impactedTokenAmount: result.impactedTokenAmount.toString(), - }; - } - - /** - * Unfreezes tokens at a specific Spark address. - * - * @param params Unfreeze parameters including the address to unfreeze - * @returns Promise resolving to unfreeze operation results - * @throws Error if wallet is not initialized or unfreeze operation fails - */ - async unfreezeTokens(params: SparkUnfreezeTokensParams): Promise { - const wallet = this.ensureWallet(); - const result = await wallet.unfreezeTokens(params.address); - - // Update freeze status in database - try { - const tokenMetadata = await wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); - const publicKeyHash = extractIdentityPublicKey(params.address); - - if (!publicKeyHash) { - throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); - } - - // Update frozen addresses table - await this.sdk.axiosInstance.put("/api/tokenization/frozen-addresses", { - tokenId, - publicKeyHash, - projectId: this.sdk.projectId, - projectWalletId: this.walletInfo?.id, - }); - - // Log the unfreeze transaction - await this.logTransaction({ - tokenId, - type: "unfreeze", - toAddress: params.address, - amount: result.impactedTokenAmount.toString(), - metadata: { - impactedOutputIds: result.impactedOutputIds, - publicKeyHash, - }, - }); - } catch (saveError) { - console.warn("Failed to save unfreeze operation:", saveError); - } - - return { - impactedOutputIds: result.impactedOutputIds, - impactedTokenAmount: result.impactedTokenAmount.toString(), - }; - } - - - /** - * Gets an existing Spark wallet by ID and returns a SparkWalletDeveloperControlled instance. + * Gets wallet info by ID. * * @param walletId The wallet ID to retrieve - * @returns Promise resolving to a SparkWalletDeveloperControlled instance + * @returns Promise resolving to wallet info * * @example * ```typescript - * const sparkWallet = await sdk.wallet.spark.getWallet("existing-wallet-id"); - * const address = await sparkWallet.getAddress(); - * await sparkWallet.mintTokens(BigInt("1000000")); + * const walletInfo = await sdk.wallet.sparkIssuer.get("existing-wallet-id"); * ``` */ - async getWallet(walletId: string): Promise { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found - required to decrypt wallet"); - } - + async get(walletId: string): Promise { const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, ); if (status === 200) { - const sparkWalletInfo = data as Web3ProjectSparkWallet; - - const mnemonic = await decryptWithPrivateKey({ - privateKey: this.sdk.privateKey, - encryptedDataJSON: sparkWalletInfo.key, - }); - - const { wallet: issuerSparkWallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic, - options: { network: sparkWalletInfo.network }, - }); - - return new SparkIssuerWalletDeveloperControlled({ - sdk: this.sdk, - wallet: issuerSparkWallet, - walletInfo: sparkWalletInfo - }); + return data as Web3ProjectSparkWallet; } throw new Error("Failed to get Spark wallet"); @@ -417,12 +58,9 @@ export class SparkIssuerWalletDeveloperControlled { * * @example * ```typescript - * const wallets = await sdk.wallet.spark.list(); + * const wallets = await sdk.wallet.sparkIssuer.list(); * console.log(`Found ${wallets.length} Spark wallets:`); * wallets.forEach(w => console.log(`- ${w.id}: tags=[${w.tags.join(', ')}]`)); - * - * // Load a specific wallet for operations - * const wallet = await sdk.wallet.spark.getWallet(wallets[0].id); * ``` */ async list(): Promise { @@ -445,8 +83,7 @@ export class SparkIssuerWalletDeveloperControlled { * * @example * ```typescript - * const tokenizationWallets = await sdk.wallet.spark.getByTag("tokenization"); - * const wallet = await sdk.wallet.spark.getWallet(tokenizationWallets[0].id); + * const wallets = await sdk.wallet.sparkIssuer.getByTag("tokenization"); * ``` */ async getByTag(tag: string): Promise { diff --git a/src/types/spark/index.ts b/src/types/spark/index.ts index aa0cbb2..42faa95 100644 --- a/src/types/spark/index.ts +++ b/src/types/spark/index.ts @@ -247,8 +247,8 @@ export interface MintTokenParams { recipientAddress: string; } -// Export dev-wallet types export * from "./dev-wallet"; +export * from "./tokenization"; // Types copied from @buildonspark/spark-sdk since they are currently private // Source: https://github.com/buildonspark/spark-sdk diff --git a/src/types/spark/tokenization.ts b/src/types/spark/tokenization.ts new file mode 100644 index 0000000..b01e162 --- /dev/null +++ b/src/types/spark/tokenization.ts @@ -0,0 +1,139 @@ +/** + * Token transaction record from database + */ +export type TokenizationTransaction = { + id: string; + tokenId: string; + type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + chain: string; + network: string; + txHash?: string; + amount?: string; + fromAddress?: string; + toAddress?: string; + status: string; + metadata?: Record; + createdAt: string; +}; + +/** + * Frozen address information from database + */ +export type TokenizationFrozenAddress = { + id: string; + address: string; + publicKeyHash: string; + stakeKeyHash?: string; + chain: string; + network: string; + freezeReason?: string; + frozenAt: string; + createdAt: string; +}; + +/** + * Pagination info for list responses + */ +export type TokenizationPaginationInfo = { + currentPage: number; + totalPages: number; + totalCount: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +}; + +/** + * Tokenization policy from database + */ +export type TokenizationPolicy = { + tokenId: string; + projectId: string; + walletId: string; + chain: string; + network: string; + isActive: boolean; + createdAt: string; +}; + +/** + * Parameters for creating a new token. + * Requires enableTokenization: true in createWallet() or initWallet(tokenId) to be called first. + */ +export type CreateTokenParams = { + tokenName: string; + tokenTicker: string; + decimals: number; + maxSupply?: bigint; + isFreezable: boolean; +}; + +/** + * Parameters for minting tokens. + * Requires initWallet() to be called first. + */ +export type MintTokensParams = { + amount: bigint; +}; + +/** + * Parameters for transferring tokens. + * Requires initWallet() to be called first. + */ +export type TransferTokensParams = { + amount: bigint; + toAddress: string; +}; + +/** + * Parameters for burning tokens. + * Requires initWallet() to be called first. + */ +export type BurnTokensParams = { + amount: bigint; +}; + +/** + * Parameters for freezing tokens. + * Requires initWallet() to be called first. + */ +export type FreezeTokensParams = { + address: string; + freezeReason?: string; +}; + +/** + * Parameters for unfreezing tokens. + * Requires initWallet() to be called first. + */ +export type UnfreezeTokensParams = { + address: string; +}; + +/** + * Parameters for listing transactions. + * Requires initWallet() to be called first. + */ +export type ListTransactionsParams = { + type?: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + page?: number; + limit?: number; +}; + +/** + * Parameters for listing frozen addresses. + * Requires initWallet() to be called first. + */ +export type ListFrozenAddressesParams = { + includeUnfrozen?: boolean; + page?: number; + limit?: number; +}; + +/** + * Parameters for listing tokenization policies. + */ +export type ListTokenizationPoliciesParams = { + tokenId?: string; + page?: number; + limit?: number; +}; From 7667c05536cb9b4470bc4d6c22a2758b01241d1d Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Thu, 27 Nov 2025 07:39:22 +0800 Subject: [PATCH 15/30] fix: spark transfer --- src/sdk/tokenization/spark.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index 60e6cca..a372e6c 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -5,6 +5,7 @@ import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; import { Bech32mTokenIdentifier, decodeBech32mTokenIdentifier, + encodeBech32mTokenIdentifier, } from "@buildonspark/spark-sdk"; import { extractIdentityPublicKey } from "../../chains/spark/utils"; import { SparkFreezeResult } from "../../types/spark/dev-wallet"; @@ -305,8 +306,13 @@ export class TokenizationSpark { const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const network = this.walletInfo.network === "MAINNET" ? "MAINNET" : "REGTEST"; + const bech32mTokenId = encodeBech32mTokenIdentifier({ + tokenIdentifier: tokenMetadata.rawTokenIdentifier, + network, + }); const txHash = await this.wallet.transferTokens({ - tokenIdentifier: tokenId as Bech32mTokenIdentifier, + tokenIdentifier: bech32mTokenId, tokenAmount: params.amount, receiverSparkAddress: params.toAddress, }); From e5b06492845e8637659f3b05fe42cf973ea8b1dc Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 15:01:56 +0800 Subject: [PATCH 16/30] feat(tokenization): init cardano tokenization and cleanup --- src/sdk/tokenization/cardano.ts | 324 ++++++++++++++++++++++++++++++ src/sdk/tokenization/index.ts | 9 +- src/sdk/tokenization/spark.ts | 11 +- src/types/cardano/tokenization.ts | 94 +++++++++ 4 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 src/sdk/tokenization/cardano.ts create mode 100644 src/types/cardano/tokenization.ts diff --git a/src/sdk/tokenization/cardano.ts b/src/sdk/tokenization/cardano.ts new file mode 100644 index 0000000..8550e5d --- /dev/null +++ b/src/sdk/tokenization/cardano.ts @@ -0,0 +1,324 @@ +import { Web3Sdk } from ".."; +import { decryptWithPrivateKey } from "../../functions"; +import { Web3ProjectCardanoWallet } from "../../types"; +import { MeshWallet } from "@meshsdk/wallet"; +import { + TokenizationTransaction, + TokenizationFrozenAddress, + TokenizationPaginationInfo, + TokenizationPolicy, + CreateTokenParams, + MintTokensParams, + TransferTokensParams, + BurnTokensParams, + FreezeTokensParams, + UnfreezeTokensParams, + ListTransactionsParams, + ListFrozenAddressesParams, + ListTokenizationPoliciesParams, +} from "../../types/cardano/tokenization"; + +export type { + CreateTokenParams, + MintTokensParams, + TransferTokensParams, + BurnTokensParams, + FreezeTokensParams, + UnfreezeTokensParams, + ListTransactionsParams, + ListFrozenAddressesParams, + ListTokenizationPoliciesParams, + TokenizationTransaction, + TokenizationFrozenAddress, + TokenizationPaginationInfo, + TokenizationPolicy, +}; + +export class TokenizationCardano { + private readonly sdk: Web3Sdk; + private wallet: MeshWallet | null = null; + private walletInfo: Web3ProjectCardanoWallet | null = null; + + constructor({ sdk }: { sdk: Web3Sdk }) { + this.sdk = sdk; + } + + setWallet(wallet: MeshWallet, walletInfo: Web3ProjectCardanoWallet): void { + this.wallet = wallet; + this.walletInfo = walletInfo; + } + + getWalletId(): string | null { + return this.walletInfo?.id ?? null; + } + + private async initWalletByWalletId(walletId: string): Promise { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found - required to decrypt wallet"); + } + + const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "testnet"; + const { data, status } = await this.sdk.axiosInstance.get( + `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=cardano&network=${networkParam}`, + ); + + if (status !== 200) { + throw new Error("Failed to get Cardano wallet"); + } + + const walletInfo = data as Web3ProjectCardanoWallet; + + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: walletInfo.key, + }); + + const networkId = this.sdk.network === "mainnet" ? 1 : 0; + const wallet = new MeshWallet({ + networkId, + fetcher: undefined, + submitter: undefined, + key: { + type: "mnemonic", + words: mnemonic.split(" "), + }, + }); + + this.wallet = wallet; + this.walletInfo = walletInfo; + } + + async initWallet(tokenId: string): Promise { + const policy = await this.getTokenizationPolicy(tokenId); + await this.initWalletByWalletId(policy.walletId); + return policy; + } + + async createToken(params: CreateTokenParams): Promise<{ + txId: string; + tokenId: string; + walletId: string; + }> { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Use createWallet({ enableTokenization: true }) first."); + } + + // TODO: Implement CIP113 token creation + // 1. Create minting policy script + // 2. Build and submit token creation transaction + // 3. Return token ID (policy ID + asset name) + throw new Error("Cardano token creation not yet implemented"); + } + + async mintTokens(params: MintTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Implement CIP113 minting + throw new Error("Cardano token minting not yet implemented"); + } + + async getTokenBalance(): Promise<{ balance: string }> { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Query UTxOs for token balance + throw new Error("Cardano token balance query not yet implemented"); + } + + async getTokenMetadata() { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Query on-chain metadata (CIP25/CIP68) + throw new Error("Cardano token metadata query not yet implemented"); + } + + async transferTokens(params: TransferTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Build and submit transfer transaction + throw new Error("Cardano token transfer not yet implemented"); + } + + async burnTokens(params: BurnTokensParams): Promise { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Build and submit burn transaction + throw new Error("Cardano token burning not yet implemented"); + } + + async freezeTokens(params: FreezeTokensParams): Promise<{ + txId?: string; + address: string; + }> { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Implement freeze logic (on-chain or off-chain) + throw new Error("Cardano token freezing not yet implemented"); + } + + async unfreezeTokens(params: UnfreezeTokensParams): Promise<{ + txId?: string; + address: string; + }> { + if (!this.wallet || !this.walletInfo) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Implement unfreeze logic + throw new Error("Cardano token unfreezing not yet implemented"); + } + + async getFrozenAddresses(params?: ListFrozenAddressesParams): Promise<{ + frozenAddresses: TokenizationFrozenAddress[]; + pagination: TokenizationPaginationInfo; + }> { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Get token ID from wallet metadata + const tokenId = ""; + + const { includeUnfrozen = false, page = 1, limit = 15 } = params || {}; + + const { data, status } = await this.sdk.axiosInstance.get( + `api/tokenization/frozen-addresses`, + { + params: { + tokenId, + projectId: this.sdk.projectId, + includeUnfrozen, + page, + limit, + }, + } + ); + + if (status === 200) { + return data as { + frozenAddresses: TokenizationFrozenAddress[]; + pagination: TokenizationPaginationInfo; + }; + } + + throw new Error("Failed to get frozen addresses"); + } + + async getTransactions(params?: ListTransactionsParams): Promise<{ + transactions: TokenizationTransaction[]; + pagination: TokenizationPaginationInfo; + }> { + if (!this.wallet) { + throw new Error("No wallet loaded. Call initWallet(tokenId) first."); + } + + // TODO: Get token ID from wallet metadata + const tokenId = ""; + + const { type, page = 1, limit = 50 } = params || {}; + + const { data, status } = await this.sdk.axiosInstance.get( + `api/tokenization/transactions`, + { + params: { + tokenId, + projectId: this.sdk.projectId, + ...(type && { type }), + page, + limit, + }, + } + ); + + if (status === 200) { + return data as { + transactions: TokenizationTransaction[]; + pagination: TokenizationPaginationInfo; + }; + } + + throw new Error("Failed to get transactions"); + } + + async getTokenizationPolicies(params?: ListTokenizationPoliciesParams): Promise<{ + tokens: TokenizationPolicy[]; + pagination: TokenizationPaginationInfo; + }> { + const { tokenId, page = 1, limit = 15 } = params || {}; + + const { data, status } = await this.sdk.axiosInstance.get( + `api/tokenization/policies`, + { + params: { + projectId: this.sdk.projectId, + chain: "cardano", + ...(tokenId && { tokenId }), + page, + limit, + }, + } + ); + + if (status === 200) { + return data as { + tokens: TokenizationPolicy[]; + pagination: TokenizationPaginationInfo; + }; + } + + throw new Error("Failed to get tokenization policies"); + } + + async getTokenizationPolicy(tokenId: string): Promise { + const { tokens } = await this.getTokenizationPolicies({ tokenId, limit: 1 }); + const policy = tokens[0]; + + if (!policy) { + throw new Error("Tokenization policy not found"); + } + + return policy; + } + + private async logTransaction(params: { + tokenId: string; + walletInfo: Web3ProjectCardanoWallet; + type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + txHash?: string; + amount?: string; + fromAddress?: string; + toAddress?: string; + status?: string; + metadata?: Record; + }): Promise { + try { + await this.sdk.axiosInstance.post("/api/tokenization/transactions", { + tokenId: params.tokenId, + projectId: this.sdk.projectId, + projectWalletId: params.walletInfo.id, + type: params.type, + chain: "cardano", + network: this.sdk.network, + txHash: params.txHash, + amount: params.amount, + fromAddress: params.fromAddress, + toAddress: params.toAddress, + status: params.status || "success", + metadata: params.metadata, + }); + } catch (error) { + console.warn(`Failed to log ${params.type} transaction:`, error); + } + } +} diff --git a/src/sdk/tokenization/index.ts b/src/sdk/tokenization/index.ts index 94c4887..19448e8 100644 --- a/src/sdk/tokenization/index.ts +++ b/src/sdk/tokenization/index.ts @@ -1,14 +1,15 @@ import { Web3Sdk } from ".."; import { TokenizationSpark } from "./spark"; +import { TokenizationCardano } from "./cardano"; export class Tokenization { private readonly sdk: Web3Sdk; spark: TokenizationSpark; + cardano: TokenizationCardano; constructor({ sdk }: { sdk: Web3Sdk }) { - { - this.sdk = sdk; - this.spark = new TokenizationSpark({ sdk: this.sdk }); - } + this.sdk = sdk; + this.spark = new TokenizationSpark({ sdk: this.sdk }); + this.cardano = new TokenizationCardano({ sdk: this.sdk }); } } diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index a372e6c..b8242dd 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -204,12 +204,17 @@ export class TokenizationSpark { }); const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const network = this.walletInfo.network === "MAINNET" ? "MAINNET" : "REGTEST"; + const tokenId = encodeBech32mTokenIdentifier({ + tokenIdentifier: tokenMetadata.rawTokenIdentifier, + network, + }); // Save tokenization policy to database try { await this.sdk.axiosInstance.post("/api/tokenization/tokens", { - tokenId, + tokenId: tokenIdHex, projectId: this.sdk.projectId, walletId: this.walletInfo.id, chain: "spark", @@ -218,7 +223,7 @@ export class TokenizationSpark { // Log the create transaction await this.logTransaction({ - tokenId, + tokenId: tokenIdHex, walletInfo: this.walletInfo, type: "create", txHash: txId, diff --git a/src/types/cardano/tokenization.ts b/src/types/cardano/tokenization.ts new file mode 100644 index 0000000..90acdf0 --- /dev/null +++ b/src/types/cardano/tokenization.ts @@ -0,0 +1,94 @@ +export type TokenizationTransaction = { + id: string; + tokenId: string; + type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + chain: string; + network: string; + txHash?: string; + amount?: string; + fromAddress?: string; + toAddress?: string; + status: string; + metadata?: Record; + createdAt: string; +}; + +export type TokenizationFrozenAddress = { + id: string; + address: string; + publicKeyHash: string; + stakeKeyHash?: string; + chain: string; + network: string; + isFrozen: boolean; + freezeReason?: string; + frozenAt: string; + unfrozenAt?: string | null; + createdAt: string; +}; + +export type TokenizationPaginationInfo = { + currentPage: number; + totalPages: number; + totalCount: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +}; + +export type TokenizationPolicy = { + tokenId: string; + projectId: string; + walletId: string; + chain: string; + network: string; + isActive: boolean; + createdAt: string; +}; + +export type CreateTokenParams = { + tokenName: string; + tokenTicker: string; + decimals: number; + maxSupply?: bigint; + isFreezable: boolean; +}; + +export type MintTokensParams = { + amount: bigint; +}; + +export type TransferTokensParams = { + amount: bigint; + toAddress: string; +}; + +export type BurnTokensParams = { + amount: bigint; +}; + +export type FreezeTokensParams = { + address: string; + freezeReason?: string; +}; + +export type UnfreezeTokensParams = { + address: string; +}; + +export type ListTransactionsParams = { + type?: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; + page?: number; + limit?: number; +}; + +export type ListFrozenAddressesParams = { + includeUnfrozen?: boolean; + page?: number; + limit?: number; +}; + +export type ListTokenizationPoliciesParams = { + tokenId?: string; + page?: number; + limit?: number; +}; From c754b778d7e0b6d33b4c4b3bd0d6a090cfe7cc23 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 15:02:16 +0800 Subject: [PATCH 17/30] refactor: remove cardano tokenization for dev wallet --- .../wallet-developer-controlled/cardano.ts | 170 ------------------ 1 file changed, 170 deletions(-) diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index acb69f5..911ae42 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -2,17 +2,6 @@ import { Web3Sdk } from ".."; import { MeshWallet } from "@meshsdk/wallet"; import { decryptWithPrivateKey } from "../../functions"; import { Web3ProjectCardanoWallet, TokenCreationParams } from "../../types"; -import { - CardanoTransactionResult, - CardanoTokenBalanceParams, - CardanoTokenBalanceResult, - CardanoTransferTokensParams, - CardanoBatchTransferParams, - CardanoFreezeTokensParams, - CardanoUnfreezeTokensParams, - CardanoFreezeResult, - CardanoBurnTokensParams, -} from "../../types/cardano/dev-wallet"; /** * CardanoWalletDeveloperControlled - Manages Cardano-specific developer-controlled wallets @@ -109,163 +98,4 @@ export class CardanoWalletDeveloperControlled { throw new Error("Failed to get Cardano wallets by tag"); } - - // ================================================================================= - // TOKEN OPERATIONS - UNIMPLEMENTED (Future CIP-113 Integration) - // ================================================================================= - - /** - * Creates a new Cardano token (CIP-113 programmable tokens). - * - * @param params - Token creation parameters (same interface as Spark) - * @returns Promise that resolves to transaction information - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - * - * @example - * ```typescript - * const result = await cardanoWallet.createToken({ - * tokenName: "My Token", - * tokenTicker: "MTK", - * decimals: 6, - * maxSupply: "1000000", - * isFreezable: true - * }); - * ``` - */ - async createToken( - params: TokenCreationParams, - ): Promise { - throw new Error( - "Cardano token creation not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Mints Cardano tokens to specified address or issuer wallet. - * - * @param params - Minting parameters - * @returns Promise that resolves to transaction information - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async mintTokens(params: { - tokenId: string; - amount: string; - address?: string; - }): Promise { - throw new Error( - "Cardano token minting not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Transfers Cardano tokens between addresses. - * - * @param params - Transfer parameters - * @returns Promise that resolves to transaction information - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async transferTokens( - params: CardanoTransferTokensParams, - ): Promise { - throw new Error( - "Cardano token transfer not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Batch transfers Cardano tokens to multiple recipients. - * - * @param params - Batch transfer parameters - * @returns Promise that resolves to transaction information - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async batchTransferTokens( - params: CardanoBatchTransferParams, - ): Promise { - throw new Error( - "Cardano batch token transfer not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Gets token balance for a Cardano address. - * - * @param params - Balance query parameters - * @returns Promise that resolves to balance information - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async getTokenBalance( - params: CardanoTokenBalanceParams, - ): Promise { - throw new Error( - "Cardano token balance query not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Gets metadata for a specific Cardano token. - * - * @param params - Token metadata query parameters - * @returns Promise that resolves to token metadata - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async getTokenMetadata(params: { tokenId: string }): Promise { - throw new Error( - "Cardano token metadata query not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Burns Cardano tokens permanently from circulation. - * - * @param params - Token burning parameters - * @returns Promise that resolves to transaction information - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async burnTokens( - params: CardanoBurnTokensParams, - ): Promise { - throw new Error( - "Cardano token burning not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Freezes Cardano tokens at specific address (CIP-113 compliance). - * - * @param params - Freeze parameters - * @returns Promise that resolves to freeze operation details - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async freezeTokens( - params: CardanoFreezeTokensParams, - ): Promise { - throw new Error( - "Cardano token freezing not implemented yet - awaiting CIP-113 standard", - ); - } - - /** - * Unfreezes Cardano tokens at specific address (CIP-113 compliance). - * - * @param params - Unfreeze parameters - * @returns Promise that resolves to unfreeze operation details - * - * @throws {Error} Method not implemented yet - awaiting CIP-113 - */ - async unfreezeTokens( - params: CardanoUnfreezeTokensParams, - ): Promise { - throw new Error( - "Cardano token unfreezing not implemented yet - awaiting CIP-113 standard", - ); - } } From fe74786128a5e479379903d60fb4204854dde5a0 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 15:10:46 +0800 Subject: [PATCH 18/30] refactor: remove unnecessary files and codes --- src/sdk/sponsorship/index.ts | 5 +- src/spark/web3-spark-wallet.ts | 613 ------------------------- src/types/window/open-window-params.ts | 80 ---- src/types/window/open-window-result.ts | 56 --- 4 files changed, 2 insertions(+), 752 deletions(-) delete mode 100644 src/spark/web3-spark-wallet.ts diff --git a/src/sdk/sponsorship/index.ts b/src/sdk/sponsorship/index.ts index c0ec8f8..c20bc39 100644 --- a/src/sdk/sponsorship/index.ts +++ b/src/sdk/sponsorship/index.ts @@ -340,9 +340,8 @@ export class Sponsorship { } private async getSponsorWallet(projectWalletId: string) { - // For sponsorship, we use direct Cardano wallet access - const walletResult = await this.sdk.wallet.cardano.getWallet(projectWalletId); - return walletResult.wallet; + const walletResult = await this.sdk.wallet.getWallet(projectWalletId, "cardano"); + return walletResult.cardanoWallet; } /** diff --git a/src/spark/web3-spark-wallet.ts b/src/spark/web3-spark-wallet.ts deleted file mode 100644 index b719a72..0000000 --- a/src/spark/web3-spark-wallet.ts +++ /dev/null @@ -1,613 +0,0 @@ -import axios, { AxiosInstance } from "axios"; -import { ApiError } from "../wallet-user-controlled"; -import { OpenWindowResult, Web3AuthProvider } from "../types"; -import * as Spark from "../types/spark"; -import { openWindow } from "../functions"; -import { getSparkAddressFromPubkey } from "../chains"; - -export type ValidSparkNetwork = "MAINNET" | "REGTEST"; - -export type EnableSparkWalletOptions = { - network: ValidSparkNetwork; - sparkscanApiKey?: string; - projectId?: Web3AuthProvider; - appUrl?: string; - baseUrl?: string; - key?: { - type: "address"; - address: string; - identityPublicKey?: string; - }; -}; - -/** - * Web3SparkWallet - Spark Wallet Implementation for Mesh Web3 SDK - * - * Provides Spark Wallet API-compliant implementation for Bitcoin Layer 2 operations, - * token transfers, and message signing as part of the Mesh Web3 SDK. - * - * @example - * ```typescript - * // Initialize wallet - * const wallet = await Web3SparkWallet.enable({ - * network: "REGTEST", - * projectId: "your-project-id", - * appUrl: "https://your-app.com" - * }); - * - * // Get wallet information - * const address = await wallet.getSparkAddress(); - * const publicKey = await wallet.getIdentityPublicKey(); - * const balance = await wallet.getBalance(); - * - * // Transfer Bitcoin - * const txId = await wallet.transfer({ - * receiverSparkAddress: "spark1q...", - * amountSats: 100000 - * }); - * ``` - */ -export class Web3SparkWallet { - private readonly _axiosInstance: AxiosInstance; - readonly network: ValidSparkNetwork; - private sparkAddress: string = ""; - private publicKey: string = ""; - private projectId?: string; - private appUrl?: string; - - constructor(options: EnableSparkWalletOptions) { - this._axiosInstance = axios.create({ - baseURL: options.baseUrl || "https://api.sparkscan.io", - headers: { - Accept: "application/json", - ...(options.sparkscanApiKey && { - Authorization: `Bearer ${options.sparkscanApiKey}`, - }), - }, - }); - this.network = options.network; - this.projectId = options.projectId; - this.appUrl = options.appUrl; - - if (options.key?.type === "address") { - this.sparkAddress = options.key.address; - this.publicKey = options.key.identityPublicKey || ""; - } - } - - /** - * Enables and initializes a Web3SparkWallet instance - * @param options - Configuration options for the wallet - * @param options.network - Network to connect to ("MAINNET" | "REGTEST") - * @param options.sparkscanApiKey - Optional API key for Sparkscan - * @param options.projectId - Project ID for authentication - * @param options.appUrl - Application URL for iframe communication - * @param options.baseUrl - Optional custom base URL for API calls - * @param options.key - Optional pre-existing wallet key information - * @returns Promise resolving to an initialized Web3SparkWallet instance - * @throws ApiError if wallet initialization fails or user declines - */ - static async enable( - options: EnableSparkWalletOptions, - ): Promise { - if (options.key?.type === "address") { - return new Web3SparkWallet(options); - } - - const networkId = options.network === "MAINNET" ? 1 : 0; - const res: OpenWindowResult = await openWindow( - { - method: "enable", - projectId: options.projectId!, - directTo: options.projectId!, - refreshToken: "undefined", - networkId: String(networkId), - keepWindowOpen: "false", - }, - options.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined to enable Spark wallet.", - }); - - if (res.data.method !== "enable") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - const publicKey = options.network === "MAINNET" - ? res.data.sparkMainnetPubKeyHash - : res.data.sparkRegtestPubKeyHash; - - const sparkAddress = getSparkAddressFromPubkey(publicKey, options.network); - - return new Web3SparkWallet({ - network: options.network, - sparkscanApiKey: options.sparkscanApiKey, - projectId: options.projectId, - appUrl: options.appUrl, - key: { - type: "address", - address: sparkAddress, - identityPublicKey: publicKey, - }, - }); - } - - /** - * Create a new token - */ - async createToken(params: Spark.TokenCreationParams): Promise { - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Token creation requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-create-token", - projectId: this.projectId, - networkId: String(networkId), - tokenName: params.tokenName, - tokenTicker: params.tokenTicker, - decimals: String(params.decimals), - maxSupply: params.maxSupply?.toString() ?? "0", - isFreezable: params.isFreezable ? "true" : "false", - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined to create token.", - }); - - if (res.data.method !== "spark-create-token") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txId || res.data.tokenId || ''; - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to create token: " + error, - }); - } - } - - /** - * Mint tokens to a specific address (issuer only) - * @param params - Minting parameters - * @returns Promise resolving to the mint transaction ID - */ - async mintTokens(params: Spark.MintTokenParams): Promise; - /** - * Mint tokens to a specific address (issuer only) - legacy signature - * @param tokenIdentifier - The token identifier (btkn1...) - * @param amount - Amount of tokens to mint in base units - * @param recipientAddress - Recipient address - * @returns Promise resolving to the mint transaction ID - */ - async mintTokens(tokenIdentifier: string, amount: bigint, recipientAddress: string): Promise; - async mintTokens( - paramsOrTokenIdentifier: Spark.MintTokenParams | string, - amount?: bigint, - recipientAddress?: string - ): Promise { - // Handle both signatures - const params: Spark.MintTokenParams = typeof paramsOrTokenIdentifier === 'string' - ? { tokenIdentifier: paramsOrTokenIdentifier, amount: amount!, recipientAddress: recipientAddress! } - : paramsOrTokenIdentifier; - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Token minting requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-mint-tokens", - projectId: this.projectId, - networkId: String(networkId), - tokenIdentifier: params.tokenIdentifier, - amount: params.amount.toString(), - recipientAddress: params.recipientAddress, - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined to mint tokens.", - }); - - if (res.data.method !== "spark-mint-tokens") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txId; - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to mint tokens: " + error, - }); - } - } - - /** - * Freeze a Spark address from transferring tokens - * Note: Spark does not support clawback, only freeze/unfreeze - * @param tokenIdentifier - The token identifier (btkn1...) - * @param address - The Spark address to freeze - * @param reason - Optional reason for freezing - * @returns Promise resolving to the freeze transaction ID - */ - async freezeTokens(tokenIdentifier: string, address: string, reason?: string): Promise { - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Token freezing requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-freeze-address", - projectId: this.projectId, - networkId: String(networkId), - tokenIdentifier, - address, - reason, - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined to freeze tokens.", - }); - - if (res.data.method !== "spark-freeze-address") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txId; - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to freeze tokens: " + error, - }); - } - } - - /** - * Unfreeze a Spark address to allow token transfers - * @param tokenIdentifier - The token identifier (btkn1...) - * @param address - The Spark address to unfreeze - * @returns Promise resolving to the unfreeze transaction ID - */ - async unfreezeTokens(tokenIdentifier: string, address: string): Promise { - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Token unfreezing requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-unfreeze-address", - projectId: this.projectId, - networkId: String(networkId), - tokenIdentifier, - address, - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined to unfreeze tokens.", - }); - - if (res.data.method !== "spark-unfreeze-address") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txId; - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to unfreeze tokens: " + error, - }); - } - } - - /** - * Burn tokens permanently from circulation - * @param tokenIdentifier - The token identifier (btkn1...) - * @param amount - Amount of tokens to burn in base units - * @returns Promise resolving to the burn transaction ID - */ - async burnTokens(tokenIdentifier: string, amount: bigint): Promise { - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Token burning requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-burn-tokens", - projectId: this.projectId, - networkId: String(networkId), - tokenIdentifier, - amount: amount.toString(), - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined to burn tokens.", - }); - - if (res.data.method !== "spark-burn-tokens") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txId; - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to burn tokens: " + error, - }); - } - } - - /** - * Transfer Spark tokens to a single recipient - * @param tokenIdentifier - The token identifier (btkn1...) - * @param recipientAddress - Recipient's Spark address - * @param amount - Amount to transfer in base units - * @returns Promise resolving to the transfer transaction ID - */ - async transferTokens(tokenIdentifier: string, recipientAddress: string, amount: bigint): Promise { - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Token transfer requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-transfer-tokens", - projectId: this.projectId, - networkId: String(networkId), - tokenIdentifier, - amount: amount.toString(), - recipientAddress, - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined the token transfer.", - }); - - if (res.data.method !== "spark-transfer-tokens") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txId; - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to transfer tokens: " + error, - }); - } - } - - /** - * Batch transfer Spark tokens to multiple recipients - * @param tokenIdentifier - The token identifier (btkn1...) - * @param transfers - Array of recipient addresses and amounts - * @returns Promise resolving to array of transaction IDs - */ - async batchTransferTokens( - tokenIdentifier: string, - transfers: Array<{ recipientAddress: string; amount: bigint }> - ): Promise { - if (!this.projectId || !this.appUrl) { - throw new ApiError({ - code: 1, - info: "Batch transfer requires projectId and appUrl for authentication", - }); - } - - try { - const networkId = this.network === "MAINNET" ? 1 : 0; - - const res: OpenWindowResult = await openWindow( - { - method: "spark-batch-transfer", - projectId: this.projectId, - networkId: String(networkId), - tokenIdentifier, - transfers: JSON.stringify(transfers.map(t => ({ - recipientAddress: t.recipientAddress, - amount: t.amount.toString(), - }))), - }, - this.appUrl, - ); - - if (res.success === false) - throw new ApiError({ - code: 3, - info: "UserDeclined - User declined the batch transfer.", - }); - - if (res.data.method !== "spark-batch-transfer") { - throw new ApiError({ - code: 2, - info: "Received the wrong response from the iframe.", - }); - } - - return res.data.txIds || (res.data.txId ? [res.data.txId] : []); - } catch (error) { - throw new ApiError({ - code: 4, - info: "Failed to execute batch transfer: " + error, - }); - } - } - - /** - * Get all token balances for an address using Sparkscan API - * Follows the official API: GET /v1/address/{address}/tokens - * @param address - Optional address to query (defaults to wallet address) - * @returns Promise resolving to AddressTokensResponse with all token balances - * @see https://docs.sparkscan.io/api/address#get-address-tokens - */ - async getAddressTokens(address?: string): Promise { - try { - const targetAddress = address || this.sparkAddress; - if (!targetAddress) { - throw new Error("Address is required"); - } - - const params = new URLSearchParams({ - network: this.network, - }); - - const response = await this._axiosInstance.get( - `/v1/address/${targetAddress}/tokens?${params.toString()}` - ); - - return response.data; - } catch (error) { - throw new ApiError({ - code: 5, - info: `Failed to get address tokens: ${error}`, - }); - } - } - - /** - * Get the balance of a specific token for the current wallet address - * @param tokenIdentifier - The token identifier to check balance for - * @param address - Optional specific address to check (defaults to wallet address) - * @returns Promise resolving to the token balance as a string, or "0" if not found - */ - async getTokenBalance(tokenIdentifier: string, address?: string): Promise { - try { - const tokensResponse = await this.getAddressTokens(address); - - const token = tokensResponse.tokens?.find( - (token) => token.tokenIdentifier === tokenIdentifier || token.tokenAddress === tokenIdentifier - ); - - return token ? token.balance.toString() : "0"; - } catch (error) { - throw new ApiError({ - code: 5, - info: `Failed to get token balance: ${error}`, - }); - } - } - - /** - * Query token transactions using Sparkscan API - * Follows the official API: GET /v1/tokens/{identifier}/transactions - * @param tokenIdentifier - Required token identifier (btkn1...) - * @param limit - Optional limit (default: 25, max: 100) - * @param offset - Optional offset for pagination (default: 0) - * @returns Promise resolving to TokenTransactionsResponse - * @see https://docs.sparkscan.io/api/tokens#get-token-transactions - */ - async queryTokenTransactions( - tokenIdentifier: string, - limit: number = 25, - offset: number = 0 - ): Promise { - try { - if (!tokenIdentifier) { - throw new Error("Token identifier is required"); - } - - const queryLimit = Math.min(Math.max(1, limit), 100); - - const params = new URLSearchParams({ - network: this.network, - limit: queryLimit.toString(), - offset: offset.toString(), - }); - - const response = await this._axiosInstance.get( - `/v1/tokens/${tokenIdentifier}/transactions?${params.toString()}` - ); - - return response.data; - } catch (error) { - throw new ApiError({ - code: 5, - info: `Failed to query token transactions: ${error}`, - }); - } - } -} diff --git a/src/types/window/open-window-params.ts b/src/types/window/open-window-params.ts index c880d57..959c90e 100644 --- a/src/types/window/open-window-params.ts +++ b/src/types/window/open-window-params.ts @@ -78,86 +78,6 @@ export type OpenWindowParams = networkId: string; message: string; } - /** Spark Token Operations */ - | { - method: "spark-create-token"; - projectId: string; - networkId: string; - tokenName: string; - tokenTicker: string; - decimals: string; - maxSupply: string; - isFreezable: "true" | "false"; - } - | { - method: "spark-mint-tokens"; - projectId: string; - networkId: string; - tokenIdentifier: string; - amount: string; - recipientAddress: string; - } - | { - method: "spark-burn-tokens"; - projectId: string; - networkId: string; - tokenIdentifier: string; - amount: string; - } - | { - method: "spark-freeze-address"; - projectId: string; - networkId: string; - tokenIdentifier: string; - address: string; - reason?: string; - } - | { - method: "spark-unfreeze-address"; - projectId: string; - networkId: string; - tokenIdentifier: string; - address: string; - } - | { - method: "spark-transfer-tokens"; - projectId: string; - networkId: string; - tokenIdentifier: string; - amount: string; - recipientAddress: string; - } - | { - method: "spark-batch-transfer"; - projectId: string; - networkId: string; - tokenIdentifier: string; - transfers: string; - } - | { - method: "spark-get-token-balance"; - projectId: string; - networkId: string; - tokenIdentifier: string; - } - | { - method: "spark-get-token-holders"; - projectId: string; - networkId: string; - tokenIdentifier: string; - } - | { - method: "spark-get-token-policy"; - projectId: string; - networkId: string; - tokenIdentifier: string; - } - | { - method: "spark-get-token-analytics"; - projectId: string; - networkId: string; - tokenIdentifier: string; - } /** to be deprecated */ | { method: "sign-tx"; diff --git a/src/types/window/open-window-result.ts b/src/types/window/open-window-result.ts index 6aeadd1..97b0e1c 100644 --- a/src/types/window/open-window-result.ts +++ b/src/types/window/open-window-result.ts @@ -57,62 +57,6 @@ export type OpenWindowResult = method: "spark-sign-message"; signature: string; } - /** Spark Token Operations */ - | { - method: "spark-create-token"; - txId?: string; - tokenId?: string; - } - | { - method: "spark-mint-tokens"; - txId: string; - } - | { - method: "spark-burn-tokens"; - txId: string; - } - | { - method: "spark-freeze-address"; - txId: string; - } - | { - method: "spark-unfreeze-address"; - txId: string; - } - | { - method: "spark-transfer-tokens"; - txId: string; - } - | { - method: "spark-batch-transfer"; - txIds?: string[]; - txId?: string; - } - | { - method: "spark-get-token-balance"; - balance: string; - } - | { - method: "spark-get-token-holders"; - holders: Array<{ - address: string; - balance: string; - }>; - } - | { - method: "spark-get-token-policy"; - policy: any; - } - | { - method: "spark-get-token-analytics"; - analytics: { - totalSupply: string; - circulatingSupply: string; - holdersCount: number; - transactionsCount: number; - frozenAddressesCount: number; - }; - } /** to be deprecated */ | { method: "sign-data"; From 54982bd74c40290fd863351350a56fd75cce5931 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 15:14:05 +0800 Subject: [PATCH 19/30] fix: sponsorship is for cardano only --- src/sdk/sponsorship/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdk/sponsorship/index.ts b/src/sdk/sponsorship/index.ts index c20bc39..54b04c5 100644 --- a/src/sdk/sponsorship/index.ts +++ b/src/sdk/sponsorship/index.ts @@ -340,8 +340,8 @@ export class Sponsorship { } private async getSponsorWallet(projectWalletId: string) { - const walletResult = await this.sdk.wallet.getWallet(projectWalletId, "cardano"); - return walletResult.cardanoWallet; + const walletResult = await this.sdk.wallet.cardano.getWallet(projectWalletId); + return walletResult.wallet; } /** From 565e9db55eed17d920f28a91cba38e4b79d344f0 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 15:14:18 +0800 Subject: [PATCH 20/30] fix: update docs to be accurate --- src/sdk/tokenization/spark.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index b8242dd..72be6d1 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -48,17 +48,21 @@ export type { * ```typescript * const sdk = new Web3Sdk({ ... }); * - * // Create a new token (automatically creates a new wallet) + * // Create a new token (requires wallet with enableTokenization: true) + * const { info } = await sdk.wallet.createWallet({ + * tags: ["tokenization"], + * enableTokenization: true, + * }); * const result = await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, * isFreezable: true, * }); - * // result contains { txId, tokenId, walletId } + * // result contains { txId, tokenId (bech32m), walletId } * * // Load an existing token by ID (initializes wallet from policy) - * await sdk.tokenization.spark.initWalletByTokenId("token-id-hex"); + * await sdk.tokenization.spark.initWallet("btknrt1..."); * * // Perform operations on the loaded token * await sdk.tokenization.spark.mintTokens({ amount: BigInt("1000000") }); From 1f04cee987ba9a7d933a399f3d2bfd3ef6aedd54 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 15:30:34 +0800 Subject: [PATCH 21/30] feat: update inline docs in sdk --- .../wallet-developer-controlled/cardano.ts | 50 ++++++++++- src/sdk/wallet-developer-controlled/index.ts | 87 ++++++------------- src/types/core/multi-chain.ts | 3 +- 3 files changed, 76 insertions(+), 64 deletions(-) diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index 911ae42..de05080 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -4,7 +4,22 @@ import { decryptWithPrivateKey } from "../../functions"; import { Web3ProjectCardanoWallet, TokenCreationParams } from "../../types"; /** - * CardanoWalletDeveloperControlled - Manages Cardano-specific developer-controlled wallets + * CardanoWalletDeveloperControlled - Manages Cardano-specific developer-controlled wallets. + * + * Provides wallet management operations for Cardano wallets. + * + * @example + * ```typescript + * // List all Cardano wallets + * const wallets = await sdk.wallet.cardano.getWallets(); + * + * // Get wallets by tag + * const treasuryWallets = await sdk.wallet.cardano.getWalletsByTag("treasury"); + * + * // Get a specific wallet with initialized MeshWallet + * const { info, wallet } = await sdk.wallet.cardano.getWallet("wallet-id"); + * const addresses = wallet.getAddresses(); + * ``` */ export class CardanoWalletDeveloperControlled { readonly sdk: Web3Sdk; @@ -18,7 +33,15 @@ export class CardanoWalletDeveloperControlled { } /** - * Retrieves all Cardano wallets for the project + * Retrieves all Cardano wallets for the project. + * + * @returns Promise resolving to array of wallet information + * + * @example + * ```typescript + * const wallets = await sdk.wallet.cardano.getWallets(); + * console.log(`Found ${wallets.length} Cardano wallets`); + * ``` */ async getWallets(): Promise { const { data, status } = await this.sdk.axiosInstance.get( @@ -33,7 +56,18 @@ export class CardanoWalletDeveloperControlled { } /** - * Retrieves a specific Cardano wallet by ID + * Retrieves a specific Cardano wallet by ID and initializes a MeshWallet instance. + * + * @param walletId - The wallet ID to retrieve + * @param decryptKey - If true, returns the decrypted mnemonic in wallet info + * @returns Promise resolving to wallet info and initialized MeshWallet + * + * @example + * ```typescript + * const { info, wallet } = await sdk.wallet.cardano.getWallet("wallet-id"); + * const addresses = wallet.getAddresses(); + * console.log("Base address:", addresses.baseAddressBech32); + * ``` */ async getWallet( walletId: string, @@ -81,7 +115,15 @@ export class CardanoWalletDeveloperControlled { } /** - * Get Cardano wallets by tag + * Gets Cardano wallets filtered by tag. + * + * @param tag - The tag to filter by + * @returns Promise resolving to array of matching wallet information + * + * @example + * ```typescript + * const wallets = await sdk.wallet.cardano.getWalletsByTag("treasury"); + * ``` */ async getWalletsByTag(tag: string): Promise { if (this.sdk.privateKey === undefined) { diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index bf75c7f..6807179 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -15,30 +15,24 @@ import { v4 as uuidv4 } from "uuid"; /** * The `WalletDeveloperControlled` class provides functionality for managing developer-controlled wallets - * within a Web3 project. + * within a Web3 project. Supports multi-chain wallets with a shared mnemonic for Spark and Cardano. * * @example * ```typescript - * // โœ… Create wallet with both chains - * const { sparkWallet, cardanoWallet } = await sdk.wallet.createWallet({ - * tags: ["tokenization"], - * network: "MAINNET" + * // Create a new multi-chain wallet + * const { info, sparkIssuerWallet, cardanoWallet } = await sdk.wallet.createWallet({ + * tags: ["treasury"], * }); * - * // Use Spark wallet directly - * await sparkWallet.createToken({ - * tokenName: "MyToken", - * tokenTicker: "MTK", - * decimals: 8, - * isFreezable: true - * }); + * // Load an existing wallet by ID + * const { info, sparkWallet, cardanoWallet } = await sdk.wallet.initWallet("wallet-id"); * - * // Load existing wallet - * const { sparkWallet: existingWallet } = await sdk.wallet.initWallet("wallet-id"); - * await existingWallet.mintTokens(BigInt("1000000")); + * // Get a wallet for a specific chain + * const { cardanoWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); + * const { sparkIssuerWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); * - * // Or access via - * await sdk.wallet.spark.getWallet("wallet-id"); + * // List all project wallets + * const wallets = await sdk.wallet.getProjectWallets(); * ``` */ export class WalletDeveloperControlled { @@ -152,16 +146,8 @@ export class WalletDeveloperControlled { ); if (status === 200) { - const sparkWalletInfo: Web3ProjectSparkWallet = { - id: walletId, - projectId: this.sdk.projectId, - tags: options.tags || [], - key: encryptedKey, - publicKey: networkId === 1 ? mainnetPublicKey : regtestPublicKey, - network: sparkNetwork, - }; - - const cardanoWalletInfo: Web3ProjectCardanoWallet = { + // cardanoWalletInfo prepared for future Cardano tokenization support + const _cardanoWalletInfo: Web3ProjectCardanoWallet = { id: walletId, projectId: this.sdk.projectId, tags: options.tags || [], @@ -182,6 +168,15 @@ export class WalletDeveloperControlled { this.cardano = cardanoWalletDev; if (options.enableTokenization) { + const sparkWalletInfo: Web3ProjectSparkWallet = { + id: walletId, + projectId: this.sdk.projectId, + tags: options.tags || [], + key: encryptedKey, + publicKey: networkId === 1 ? mainnetPublicKey : regtestPublicKey, + network: sparkNetwork, + }; + this.sdk.tokenization.spark.setWallet(sparkWallet, sparkWalletInfo); } @@ -242,25 +237,6 @@ export class WalletDeveloperControlled { options: { network: sparkNetwork }, }); - const sparkWalletInfo: Web3ProjectSparkWallet = { - id: walletId, - projectId: this.sdk.projectId, - tags: walletInfo.tags || [], - key: walletInfo.key, - publicKey: await sparkWallet.getIdentityPublicKey(), - network: sparkNetwork, - }; - - const cardanoWalletInfo: Web3ProjectCardanoWallet = { - id: walletId, - projectId: this.sdk.projectId, - tags: walletInfo.tags || [], - key: walletInfo.key, - pubKeyHash: walletInfo.chains?.cardano?.pubKeyHash || "", - stakeCredentialHash: - walletInfo.chains?.cardano?.stakeCredentialHash || "", - }; - const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ sdk: this.sdk, }); @@ -331,25 +307,18 @@ export class WalletDeveloperControlled { instance.cardanoWallet = cardanoWallet; } - if ((chain === "spark" || !chain) && walletInfo.chains.spark && mnemonic) { + if ( + (chain === "spark" || !chain) && + walletInfo.chains.spark && + mnemonic + ) { const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ mnemonicOrSeed: mnemonic, options: { network: sparkNetwork }, }); - const sparkWalletInfo: Web3ProjectSparkWallet = { - id: projectWalletId, - projectId: this.sdk.projectId, - tags: walletInfo.tags || [], - key: walletInfo.key, - publicKey: await sparkWallet.getIdentityPublicKey(), - network: sparkNetwork, - }; - - instance.sparkIssuerWallet = new SparkIssuerWalletDeveloperControlled({ - sdk: this.sdk, - }); + instance.sparkIssuerWallet = sparkWallet; } return instance; diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index 1d94727..00b4557 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -1,5 +1,6 @@ import { MeshWallet } from "@meshsdk/wallet"; import type { SparkIssuerWalletDeveloperControlled } from "../../sdk/wallet-developer-controlled/spark-issuer"; +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; /** * Standardized network ID type (0 = testnet, 1 = mainnet) @@ -42,7 +43,7 @@ export interface MultiChainWalletInfo { export interface MultiChainWalletInstance { info: MultiChainWalletInfo; cardanoWallet?: MeshWallet; - sparkIssuerWallet?: SparkIssuerWalletDeveloperControlled; + sparkIssuerWallet?: IssuerSparkWallet; } /** From b5c208af607d525741d3aef7e3fb85fd96c8d5ea Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Fri, 28 Nov 2025 19:28:03 +0800 Subject: [PATCH 22/30] feat: clean up spark tokenization and dev wallet --- examples/developer-controlled-wallet/index.ts | 106 ++++++++---------- src/sdk/wallet-developer-controlled/index.ts | 42 +++---- .../spark-issuer.ts | 2 +- src/types/core/multi-chain.ts | 1 - 4 files changed, 64 insertions(+), 87 deletions(-) diff --git a/examples/developer-controlled-wallet/index.ts b/examples/developer-controlled-wallet/index.ts index 948d2c2..98adba5 100644 --- a/examples/developer-controlled-wallet/index.ts +++ b/examples/developer-controlled-wallet/index.ts @@ -1,10 +1,11 @@ import { Web3Sdk } from "@meshsdk/web3-sdk"; /** - * Example: Developer-Controlled Wallet with New API + * Example: Developer-Controlled Wallet * - * This example demonstrates the new wallet-first API for developer-controlled wallets. - * Perfect for token issuance, treasury management, and automated operations. + * This example demonstrates wallet management with multi-chain support (Spark and Cardano). + * Use sdk.wallet.* for wallet operations. + * Use sdk.tokenization.spark.* for token operations (see tokenization example). */ async function main() { @@ -17,67 +18,52 @@ async function main() { privateKey: "your-private-key", // Required for developer-controlled wallets }); - console.log("๐Ÿš€ Creating developer-controlled wallet..."); - + // === CREATE WALLET === + + console.log("Creating developer-controlled wallet..."); + // Create wallet with both Spark and Cardano chains (shared mnemonic) - const { sparkWallet, cardanoWallet } = await sdk.wallet.createWallet({ - tags: ["tokenization", "treasury"] + const { info, sparkIssuerWallet, cardanoWallet } = await sdk.wallet.createWallet({ + tags: ["treasury"], }); - console.log("โœ… Wallet created!"); - - // === SPARK TOKEN OPERATIONS === - - console.log("\n๐Ÿช™ Creating Spark token..."); - const tokenTxId = await sparkWallet.createToken({ - tokenName: "Example Token", - tokenTicker: "EXAM", - decimals: 8, - maxSupply: 1000000n, - isFreezable: true - }); - console.log("Token created:", tokenTxId); - - console.log("\n๐Ÿ’ฐ Minting tokens..."); - const mintTxId = await sparkWallet.mintTokens(BigInt("100000")); - console.log("Minted tokens:", mintTxId); - - console.log("\n๐Ÿ“Š Getting token info..."); - const balance = await sparkWallet.getTokenBalance(); - const metadata = await sparkWallet.getTokenMetadata(); - console.log("Token balance:", balance.balance); - console.log("Token name:", metadata.tokenName); - - console.log("\n๐Ÿ“ค Transferring tokens..."); - const transferTxId = await sparkWallet.transferTokens({ - tokenIdentifier: "your-token-identifier", - amount: BigInt("1000"), - toAddress: "spark1recipient..." - }); - console.log("Transfer complete:", transferTxId); - - // === COMPLIANCE OPERATIONS === - - console.log("\n๐Ÿšซ Freezing tokens for compliance..."); - const freezeResult = await sparkWallet.freezeTokens({ - address: "spark1suspicious...", - freezeReason: "Compliance investigation" - }); - console.log("Frozen outputs:", freezeResult.impactedOutputIds.length); + console.log("Wallet created:", info.id); - console.log("\nโœ… Unfreezing tokens..."); - const unfreezeResult = await sparkWallet.unfreezeTokens({ - address: "spark1suspicious..." - }); - console.log("Unfrozen outputs:", unfreezeResult.impactedOutputIds.length); - - // === LOADING EXISTING WALLET === - - console.log("\n๐Ÿ”„ Loading existing wallet..."); - const { sparkWallet: existingWallet } = await sdk.wallet.initWallet("existing-wallet-id"); - - const existingBalance = await existingWallet.getTokenBalance(); - console.log("Existing wallet balance:", existingBalance.balance); + // === LIST WALLETS === + + console.log("\nListing all project wallets..."); + const wallets = await sdk.wallet.getProjectWallets(); + console.log(`Found ${wallets.length} wallets`); + + // === GET WALLET BY CHAIN === + + console.log("\nGetting wallet for specific chain..."); + + // Get Cardano wallet + const { cardanoWallet: cardano } = await sdk.wallet.getWallet(info.id, "cardano"); + const addresses = cardano!.getAddresses(); + console.log("Cardano base address:", addresses.baseAddressBech32); + + // Get Spark wallet info + const sparkWalletInfo = await sdk.wallet.sparkIssuer.get(info.id); + console.log("Spark wallet public key:", sparkWalletInfo.publicKey); + + // === LOAD EXISTING WALLET === + + console.log("\nLoading existing wallet..."); + const { info: existingInfo, sparkWallet, cardanoWallet: existingCardano } = + await sdk.wallet.initWallet("existing-wallet-id"); + + console.log("Loaded wallet:", existingInfo.id); + + // === LIST BY TAG === + + console.log("\nListing wallets by tag..."); + const sparkWallets = await sdk.wallet.sparkIssuer.getByTag("treasury"); + const cardanoWallets = await sdk.wallet.cardano.getWalletsByTag("treasury"); + + console.log(`Found ${sparkWallets.length} Spark wallets with 'treasury' tag`); + console.log(`Found ${cardanoWallets.length} Cardano wallets with 'treasury' tag`); } // Run example diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index 6807179..c08764d 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -30,6 +30,7 @@ import { v4 as uuidv4 } from "uuid"; * // Get a wallet for a specific chain * const { cardanoWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); * const { sparkIssuerWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); + * const sparkAddress = await sparkIssuerWallet.getSparkAddress(); * * // List all project wallets * const wallets = await sdk.wallet.getProjectWallets(); @@ -78,7 +79,7 @@ export class WalletDeveloperControlled { } = {}, ): Promise<{ info: MultiChainWalletInfo; - sparkIssuerWallet: SparkIssuerWalletDeveloperControlled; + sparkIssuerWallet: IssuerSparkWallet; cardanoWallet: MeshWallet; }> { const project = await this.sdk.getProject(); @@ -182,7 +183,7 @@ export class WalletDeveloperControlled { return { info: walletData as MultiChainWalletInfo, - sparkIssuerWallet: sparkWalletDev, + sparkIssuerWallet: sparkWallet, cardanoWallet: cardanoWallet, }; } @@ -191,23 +192,25 @@ export class WalletDeveloperControlled { } /** - * Loads an existing developer-controlled wallet by ID and returns both chain instances. + * Loads an existing developer-controlled wallet by ID and returns both chain wallet instances. * * @param walletId - The wallet ID to load - * @returns Promise that resolves to both chain wallet instances + * @returns Promise that resolves to wallet info and initialized wallet instances * * @example * ```typescript - * const { sparkWallet, cardanoWallet } = await sdk.wallet.initWallet("wallet-id"); + * const { info, sparkWallet, cardanoWallet } = await sdk.wallet.initWallet("wallet-id"); + * + * // Get Spark wallet address + * const sparkAddress = await sparkWallet.getSparkAddress(); * - * // Use either wallet directly - * await sparkWallet.mintTokens(BigInt("1000000")); - * await cardanoWallet.sendAssets({...}); + * // Get Cardano wallet addresses + * const addresses = cardanoWallet.getAddresses(); * ``` */ async initWallet(walletId: string): Promise<{ info: MultiChainWalletInfo; - sparkWallet: SparkIssuerWalletDeveloperControlled; + sparkWallet: IssuerSparkWallet; cardanoWallet: MeshWallet; }> { if (!this.sdk.privateKey) { @@ -237,21 +240,10 @@ export class WalletDeveloperControlled { options: { network: sparkNetwork }, }); - const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ - sdk: this.sdk, - }); - - const cardanoWalletDev = new CardanoWalletDeveloperControlled({ - sdk: this.sdk - }); - - this.sparkIssuer = sparkWalletDev; - this.cardano = cardanoWalletDev; - return { info: walletInfo, - sparkWallet: sparkWalletDev, - cardanoWallet: cardanoWallet, + sparkWallet, + cardanoWallet, }; } @@ -265,10 +257,10 @@ export class WalletDeveloperControlled { * @example * ```typescript * // Load specific chain - * const { sparkWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); + * const { sparkIssuerWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); * - * // Load all available chains - * const { info, cardanoWallet, sparkWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); + * // Load Cardano chain + * const { info, cardanoWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); * ``` */ async getWallet( diff --git a/src/sdk/wallet-developer-controlled/spark-issuer.ts b/src/sdk/wallet-developer-controlled/spark-issuer.ts index 4a09283..ab89c0e 100644 --- a/src/sdk/wallet-developer-controlled/spark-issuer.ts +++ b/src/sdk/wallet-developer-controlled/spark-issuer.ts @@ -37,7 +37,7 @@ export class SparkIssuerWalletDeveloperControlled { * const walletInfo = await sdk.wallet.sparkIssuer.get("existing-wallet-id"); * ``` */ - async get(walletId: string): Promise { + async getWallet(walletId: string): Promise { const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index 00b4557..b1555cf 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -1,5 +1,4 @@ import { MeshWallet } from "@meshsdk/wallet"; -import type { SparkIssuerWalletDeveloperControlled } from "../../sdk/wallet-developer-controlled/spark-issuer"; import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; /** From 26eee75830673558eddf0c33171b427af4bc8721 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Sat, 29 Nov 2025 07:27:11 +0800 Subject: [PATCH 23/30] feat(tokenization): update transaction logging and wallet initialization --- examples/developer-controlled-wallet/index.ts | 2 +- src/sdk/tokenization/spark.ts | 143 +++++++++--------- src/sdk/wallet-developer-controlled/index.ts | 69 +++------ src/types/spark/tokenization.ts | 12 +- 4 files changed, 99 insertions(+), 127 deletions(-) diff --git a/examples/developer-controlled-wallet/index.ts b/examples/developer-controlled-wallet/index.ts index 98adba5..24f046b 100644 --- a/examples/developer-controlled-wallet/index.ts +++ b/examples/developer-controlled-wallet/index.ts @@ -45,7 +45,7 @@ async function main() { console.log("Cardano base address:", addresses.baseAddressBech32); // Get Spark wallet info - const sparkWalletInfo = await sdk.wallet.sparkIssuer.get(info.id); + const sparkWalletInfo = await sdk.wallet.sparkIssuer.getWallet(info.id); console.log("Spark wallet public key:", sparkWalletInfo.publicKey); // === LOAD EXISTING WALLET === diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index 72be6d1..5bc317b 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -2,6 +2,7 @@ import { Web3Sdk } from ".."; import { decryptWithPrivateKey } from "../../functions"; import { Web3ProjectSparkWallet } from "../../types"; import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import { v4 as uuidv4 } from "uuid"; import { Bech32mTokenIdentifier, decodeBech32mTokenIdentifier, @@ -14,6 +15,7 @@ import { TokenizationFrozenAddress, TokenizationPaginationInfo, TokenizationPolicy, + InitWalletParams, CreateTokenParams, MintTokensParams, TransferTokensParams, @@ -26,6 +28,7 @@ import { } from "../../types/spark/tokenization"; export type { + InitWalletParams, CreateTokenParams, MintTokensParams, TransferTokensParams, @@ -48,23 +51,23 @@ export type { * ```typescript * const sdk = new Web3Sdk({ ... }); * - * // Create a new token (requires wallet with enableTokenization: true) - * const { info } = await sdk.wallet.createWallet({ - * tags: ["tokenization"], - * enableTokenization: true, - * }); - * const result = await sdk.tokenization.spark.createToken({ + * // Create wallet and token + * const { info } = await sdk.wallet.createWallet({ tags: ["tokenization"] }); + * const { tokenId } = await sdk.tokenization.spark.createToken({ + * walletId: info.id, * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, * isFreezable: true, * }); - * // result contains { txId, tokenId (bech32m), walletId } * - * // Load an existing token by ID (initializes wallet from policy) - * await sdk.tokenization.spark.initWallet("btknrt1..."); + * // Load existing token by token ID + * await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); + * + * // Or load by wallet ID + * await sdk.tokenization.spark.initWallet({ walletId: "wallet-uuid" }); * - * // Perform operations on the loaded token + * // Perform token operations * await sdk.tokenization.spark.mintTokens({ amount: BigInt("1000000") }); * const balance = await sdk.tokenization.spark.getTokenBalance(); * ``` @@ -96,12 +99,9 @@ export class TokenizationSpark { /** * Internal method to initialize the wallet by wallet ID. + * If wallet instance is provided, uses it directly without decryption. */ - private async initWalletByWalletId(walletId: string): Promise { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found - required to decrypt wallet"); - } - + private async initWalletByWalletId(walletId: string, walletInstance?: IssuerSparkWallet): Promise { const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, @@ -113,43 +113,56 @@ export class TokenizationSpark { const walletInfo = data as Web3ProjectSparkWallet; - const mnemonic = await decryptWithPrivateKey({ - privateKey: this.sdk.privateKey, - encryptedDataJSON: walletInfo.key, - }); + if (walletInstance) { + this.wallet = walletInstance; + } else { + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found - required to decrypt wallet"); + } - const { wallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic, - options: { network: walletInfo.network }, - }); + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: walletInfo.key, + }); + + const { wallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic, + options: { network: walletInfo.network }, + }); + + this.wallet = wallet; + } - this.wallet = wallet; this.walletInfo = walletInfo; } /** - * Initializes the tokenization instance with a token by token ID. - * Looks up the tokenization policy to get the wallet and initializes it. - * Must be called before performing token operations on existing tokens. + * Initializes the tokenization wallet. Pass either tokenId or walletId, not both. * - * @param tokenId - The token ID (hex or bech32m format, e.g., "abc123..." or "btknrt1...") - * @returns The tokenization policy + * @param params - Either { tokenId } or { walletId } + * @returns The tokenization policy (when using tokenId) or void (when using walletId) * * @example * ```typescript - * // Using hex format - * const policy = await sdk.tokenization.spark.initWallet("abc123..."); - * // Or using bech32m format - * const policy = await sdk.tokenization.spark.initWallet("btknrt1..."); - * const metadata = await sdk.tokenization.spark.getTokenMetadata(); + * // By token ID - looks up policy and loads wallet + * const policy = await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); + * + * // By wallet ID - loads wallet directly + * await sdk.tokenization.spark.initWallet({ walletId: "wallet-uuid" }); + * + * // Then perform operations * await sdk.tokenization.spark.mintTokens({ amount: BigInt(1000) }); * ``` */ - async initWallet(tokenId: string): Promise { - const normalizedTokenId = this.normalizeTokenId(tokenId); - const policy = await this.getTokenizationPolicy(normalizedTokenId); - await this.initWalletByWalletId(policy.walletId); - return policy; + async initWallet(params: InitWalletParams): Promise { + if ("tokenId" in params && params.tokenId) { + const normalizedTokenId = this.normalizeTokenId(params.tokenId); + const policy = await this.getTokenizationPolicy(normalizedTokenId); + await this.initWalletByWalletId(policy.walletId); + return policy; + } else if ("walletId" in params && params.walletId) { + await this.initWalletByWalletId(params.walletId, params.wallet); + } } /** @@ -168,21 +181,19 @@ export class TokenizationSpark { /** * Creates a new token on the Spark network. - * Requires enableTokenization: true in createWallet() to be called first. + * Requires initWallet() to be called first. * * @param params - Token creation parameters * @returns Object containing txId, tokenId, and walletId * * @example * ```typescript - * // Create wallet with tokenization enabled - * const { info } = await sdk.wallet.createWallet({ - * tags: ["tokenization"], - * enableTokenization: true - * }); + * // Create wallet and initialize + * const { info } = await sdk.wallet.createWallet({ tags: ["tokenization"] }); + * await sdk.tokenization.spark.initWallet({ walletId: info.id }); * - * // Create token - wallet is already linked - * const result = await sdk.tokenization.spark.createToken({ + * // Create token + * const { tokenId } = await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, @@ -196,7 +207,7 @@ export class TokenizationSpark { walletId: string; }> { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Use createWallet({ enableTokenization: true }) first."); + throw new Error("No wallet loaded. Call initWallet() first."); } const txId = await this.wallet.createToken({ @@ -227,17 +238,10 @@ export class TokenizationSpark { // Log the create transaction await this.logTransaction({ + txId, tokenId: tokenIdHex, walletInfo: this.walletInfo, type: "create", - txHash: txId, - metadata: { - tokenName: params.tokenName, - tokenTicker: params.tokenTicker, - decimals: params.decimals, - maxSupply: params.maxSupply?.toString(), - isFreezable: params.isFreezable, - }, }); } catch (saveError) { console.warn("Failed to save token to database:", saveError); @@ -264,10 +268,10 @@ export class TokenizationSpark { const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); await this.logTransaction({ + txId: txHash, tokenId, walletInfo: this.walletInfo, type: "mint", - txHash, amount: params.amount.toString(), }); @@ -328,10 +332,10 @@ export class TokenizationSpark { const issuerAddress = await this.wallet.getSparkAddress(); await this.logTransaction({ + txId: txHash, tokenId, walletInfo: this.walletInfo, type: "transfer", - txHash, amount: params.amount.toString(), fromAddress: issuerAddress, toAddress: params.toAddress, @@ -358,10 +362,10 @@ export class TokenizationSpark { const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); await this.logTransaction({ + txId: txHash, tokenId, walletInfo: this.walletInfo, type: "burn", - txHash, amount: params.amount.toString(), }); @@ -407,16 +411,13 @@ export class TokenizationSpark { // Log the freeze transaction await this.logTransaction({ + txId: uuidv4(), tokenId, walletInfo: this.walletInfo, type: "freeze", + fromAddress: "Issuer Wallet", toAddress: params.address, amount: result.impactedTokenAmount.toString(), - metadata: { - freezeReason: params.freezeReason, - impactedOutputIds: result.impactedOutputIds, - publicKeyHash, - }, }); } catch (saveError) { @@ -463,15 +464,13 @@ export class TokenizationSpark { // Log the unfreeze transaction await this.logTransaction({ + txId: uuidv4(), tokenId, walletInfo: this.walletInfo, type: "unfreeze", + fromAddress: "Issuer Wallet", toAddress: params.address, amount: result.impactedTokenAmount.toString(), - metadata: { - impactedOutputIds: result.impactedOutputIds, - publicKeyHash, - }, }); } catch (saveError) { console.warn("Failed to save unfreeze operation:", saveError); @@ -624,30 +623,28 @@ export class TokenizationSpark { * Internal helper to log token transactions to the database */ private async logTransaction(params: { + txId: string; tokenId: string; walletInfo: Web3ProjectSparkWallet; type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; - txHash?: string; amount?: string; fromAddress?: string; toAddress?: string; status?: string; - metadata?: Record; }): Promise { try { await this.sdk.axiosInstance.post("/api/tokenization/transactions", { + txId: params.txId, tokenId: params.tokenId, projectId: this.sdk.projectId, projectWalletId: params.walletInfo.id, type: params.type, chain: "spark", network: params.walletInfo.network.toLowerCase(), - txHash: params.txHash, amount: params.amount, fromAddress: params.fromAddress, toAddress: params.toAddress, - status: params.status || "success", - metadata: params.metadata, + status: params.status || "confirmed", }); } catch (error) { console.warn(`Failed to log ${params.type} transaction:`, error); diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index c08764d..da9cde6 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -4,7 +4,6 @@ import { MultiChainWalletInstance, SupportedChain, } from "../../types/core/multi-chain"; -import { Web3ProjectCardanoWallet, Web3ProjectSparkWallet } from "../../types"; import { CardanoWalletDeveloperControlled } from "./cardano"; import { SparkIssuerWalletDeveloperControlled } from "./spark-issuer"; import { MeshWallet } from "@meshsdk/wallet"; @@ -52,18 +51,15 @@ export class WalletDeveloperControlled { * * @param options - Wallet creation options * @param options.tags - Optional tags for the wallet - * @param options.enableTokenization - If true, links the wallet to sdk.tokenization.spark for seamless token creation - * @returns Promise that resolves to both chain wallet instances + * @returns Promise that resolves to wallet info and chain wallet instances * * @example * ```typescript - * // With tokenization enabled - * const { info } = await sdk.wallet.createWallet({ - * tags: ["tokenization"], - * enableTokenization: true - * }); + * // Create wallet + * const { info } = await sdk.wallet.createWallet({ tags: ["tokenization"] }); * - * // createToken works seamlessly - wallet is already linked + * // For tokenization, use initWallet then createToken + * await sdk.tokenization.spark.initWallet({ walletId: info.id }); * await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", @@ -75,7 +71,6 @@ export class WalletDeveloperControlled { async createWallet( options: { tags?: string[]; - enableTokenization?: boolean; } = {}, ): Promise<{ info: MultiChainWalletInfo; @@ -124,7 +119,6 @@ export class WalletDeveloperControlled { sparkRegtestWallet.getIdentityPublicKey(), ]); - const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; const sparkWallet = networkId === 1 ? sparkMainnetWallet : sparkRegtestWallet; @@ -147,40 +141,6 @@ export class WalletDeveloperControlled { ); if (status === 200) { - // cardanoWalletInfo prepared for future Cardano tokenization support - const _cardanoWalletInfo: Web3ProjectCardanoWallet = { - id: walletId, - projectId: this.sdk.projectId, - tags: options.tags || [], - key: encryptedKey, - pubKeyHash, - stakeCredentialHash, - }; - - const sparkWalletDev = new SparkIssuerWalletDeveloperControlled({ - sdk: this.sdk, - }); - - const cardanoWalletDev = new CardanoWalletDeveloperControlled({ - sdk: this.sdk - }); - - this.sparkIssuer = sparkWalletDev; - this.cardano = cardanoWalletDev; - - if (options.enableTokenization) { - const sparkWalletInfo: Web3ProjectSparkWallet = { - id: walletId, - projectId: this.sdk.projectId, - tags: options.tags || [], - key: encryptedKey, - publicKey: networkId === 1 ? mainnetPublicKey : regtestPublicKey, - network: sparkNetwork, - }; - - this.sdk.tokenization.spark.setWallet(sparkWallet, sparkWalletInfo); - } - return { info: walletData as MultiChainWalletInfo, sparkIssuerWallet: sparkWallet, @@ -248,19 +208,19 @@ export class WalletDeveloperControlled { } /** - * Retrieves a multi-chain wallet with optional chain-specific loading. + * Retrieves a multi-chain wallet for a specific chain. * * @param walletId - The unique identifier of the wallet - * @param chain - Optional specific chain to load (performance optimization) + * @param chain - The chain to load ("spark" or "cardano") * @returns Promise that resolves to multi-chain wallet instance * * @example * ```typescript - * // Load specific chain + * // Load Spark wallet * const { sparkIssuerWallet } = await sdk.wallet.getWallet("wallet-id", "spark"); * - * // Load Cardano chain - * const { info, cardanoWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); + * // Load Cardano wallet + * const { cardanoWallet } = await sdk.wallet.getWallet("wallet-id", "cardano"); * ``` */ async getWallet( @@ -317,7 +277,10 @@ export class WalletDeveloperControlled { } /** - * Get a specific project wallet by ID + * Retrieves wallet metadata by ID. + * + * @param walletId - The unique identifier of the wallet + * @returns Promise that resolves to wallet info */ async getProjectWallet(walletId: string): Promise { const { data, status } = await this.sdk.axiosInstance.get( @@ -332,7 +295,9 @@ export class WalletDeveloperControlled { } /** - * Get all project wallets + * Retrieves all wallets for the project. + * + * @returns Promise that resolves to array of wallet info */ async getProjectWallets(): Promise { const { data, status } = await this.sdk.axiosInstance.get( diff --git a/src/types/spark/tokenization.ts b/src/types/spark/tokenization.ts index b01e162..513f5b2 100644 --- a/src/types/spark/tokenization.ts +++ b/src/types/spark/tokenization.ts @@ -1,3 +1,5 @@ +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; + /** * Token transaction record from database */ @@ -55,9 +57,17 @@ export type TokenizationPolicy = { createdAt: string; }; +/** + * Parameters for initializing wallet - either by token ID or wallet ID. + * When using walletId, you can optionally pass the wallet instance to skip decryption. + */ +export type InitWalletParams = + | { tokenId: string } + | { walletId: string; wallet?: IssuerSparkWallet }; + /** * Parameters for creating a new token. - * Requires enableTokenization: true in createWallet() or initWallet(tokenId) to be called first. + * Requires initWallet() to be called first. */ export type CreateTokenParams = { tokenName: string; From b0b9c28204c76011edd45ae9c41af8fd0a0b0bf4 Mon Sep 17 00:00:00 2001 From: WendellMorTamayo Date: Mon, 1 Dec 2025 02:55:42 +0800 Subject: [PATCH 24/30] refactor(sdk): bundle createWallet and createToken for seamless tokenization initialization --- src/sdk/tokenization/spark.ts | 117 ++++++++++++++++---------------- src/types/spark/tokenization.ts | 11 +-- 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index 5bc317b..e9f98b4 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -51,10 +51,8 @@ export type { * ```typescript * const sdk = new Web3Sdk({ ... }); * - * // Create wallet and token - * const { info } = await sdk.wallet.createWallet({ tags: ["tokenization"] }); - * const { tokenId } = await sdk.tokenization.spark.createToken({ - * walletId: info.id, + * // Create token (wallet is created automatically) + * const { tokenId, walletId } = await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", * decimals: 8, @@ -62,10 +60,7 @@ export type { * }); * * // Load existing token by token ID - * await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); - * - * // Or load by wallet ID - * await sdk.tokenization.spark.initWallet({ walletId: "wallet-uuid" }); + * const policy = await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); * * // Perform token operations * await sdk.tokenization.spark.mintTokens({ amount: BigInt("1000000") }); @@ -82,26 +77,25 @@ export class TokenizationSpark { } /** - * Sets the wallet for tokenization operations. - * @internal Called by sdk.wallet.createWallet() when enableTokenization is true. + * Gets the current wallet ID if one is loaded. */ - setWallet(wallet: IssuerSparkWallet, walletInfo: Web3ProjectSparkWallet): void { - this.wallet = wallet; - this.walletInfo = walletInfo; + getWalletId(): string | null { + return this.walletInfo?.id ?? null; } /** - * Gets the current wallet ID if one is loaded. + * Clears the currently loaded wallet state. + * Call this before creating a new token if you want to start fresh. */ - getWalletId(): string | null { - return this.walletInfo?.id ?? null; + clearWallet(): void { + this.wallet = null; + this.walletInfo = null; } /** * Internal method to initialize the wallet by wallet ID. - * If wallet instance is provided, uses it directly without decryption. */ - private async initWalletByWalletId(walletId: string, walletInstance?: IssuerSparkWallet): Promise { + private async initWalletByWalletId(walletId: string): Promise { const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, @@ -113,56 +107,44 @@ export class TokenizationSpark { const walletInfo = data as Web3ProjectSparkWallet; - if (walletInstance) { - this.wallet = walletInstance; - } else { - if (this.sdk.privateKey === undefined) { - throw new Error("Private key not found - required to decrypt wallet"); - } - - const mnemonic = await decryptWithPrivateKey({ - privateKey: this.sdk.privateKey, - encryptedDataJSON: walletInfo.key, - }); + if (this.sdk.privateKey === undefined) { + throw new Error("Private key not found - required to decrypt wallet"); + } - const { wallet } = await IssuerSparkWallet.initialize({ - mnemonicOrSeed: mnemonic, - options: { network: walletInfo.network }, - }); + const mnemonic = await decryptWithPrivateKey({ + privateKey: this.sdk.privateKey, + encryptedDataJSON: walletInfo.key, + }); - this.wallet = wallet; - } + const { wallet } = await IssuerSparkWallet.initialize({ + mnemonicOrSeed: mnemonic, + options: { network: walletInfo.network }, + }); + this.wallet = wallet; this.walletInfo = walletInfo; } /** - * Initializes the tokenization wallet. Pass either tokenId or walletId, not both. + * Initializes the tokenization wallet by token ID. * - * @param params - Either { tokenId } or { walletId } - * @returns The tokenization policy (when using tokenId) or void (when using walletId) + * @param params - { tokenId } - the token ID to load + * @returns The tokenization policy * * @example * ```typescript - * // By token ID - looks up policy and loads wallet + * // Load existing token by token ID * const policy = await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); * - * // By wallet ID - loads wallet directly - * await sdk.tokenization.spark.initWallet({ walletId: "wallet-uuid" }); - * * // Then perform operations * await sdk.tokenization.spark.mintTokens({ amount: BigInt(1000) }); * ``` */ - async initWallet(params: InitWalletParams): Promise { - if ("tokenId" in params && params.tokenId) { - const normalizedTokenId = this.normalizeTokenId(params.tokenId); - const policy = await this.getTokenizationPolicy(normalizedTokenId); - await this.initWalletByWalletId(policy.walletId); - return policy; - } else if ("walletId" in params && params.walletId) { - await this.initWalletByWalletId(params.walletId, params.wallet); - } + async initWallet(params: InitWalletParams): Promise { + const normalizedTokenId = this.normalizeTokenId(params.tokenId); + const policy = await this.getTokenizationPolicy(normalizedTokenId); + await this.initWalletByWalletId(policy.walletId); + return policy; } /** @@ -181,18 +163,23 @@ export class TokenizationSpark { /** * Creates a new token on the Spark network. - * Requires initWallet() to be called first. + * Automatically creates a new wallet if none is loaded. * * @param params - Token creation parameters * @returns Object containing txId, tokenId, and walletId * * @example * ```typescript - * // Create wallet and initialize - * const { info } = await sdk.wallet.createWallet({ tags: ["tokenization"] }); - * await sdk.tokenization.spark.initWallet({ walletId: info.id }); + * // Simple one-step token creation (creates wallet automatically) + * const { tokenId, walletId } = await sdk.tokenization.spark.createToken({ + * tokenName: "MyToken", + * tokenTicker: "MTK", + * decimals: 8, + * isFreezable: true, + * }); * - * // Create token + * // Or load existing wallet first + * await sdk.tokenization.spark.initWallet({ walletId: "existing-wallet-id" }); * const { tokenId } = await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", @@ -207,7 +194,23 @@ export class TokenizationSpark { walletId: string; }> { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Call initWallet() first."); + const { info, sparkIssuerWallet } = await this.sdk.wallet.createWallet({ + tags: ["tokenization"], + }); + + const networkParam = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; + this.walletInfo = { + id: info.id, + key: info.key, + tags: info.tags, + projectId: info.projectId, + publicKey: + networkParam === "MAINNET" + ? info.chains.spark?.mainnetPublicKey || "" + : info.chains.spark?.regtestPublicKey || "", + network: networkParam, + }; + this.wallet = sparkIssuerWallet; } const txId = await this.wallet.createToken({ diff --git a/src/types/spark/tokenization.ts b/src/types/spark/tokenization.ts index 513f5b2..c0841af 100644 --- a/src/types/spark/tokenization.ts +++ b/src/types/spark/tokenization.ts @@ -1,5 +1,3 @@ -import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; - /** * Token transaction record from database */ @@ -58,16 +56,13 @@ export type TokenizationPolicy = { }; /** - * Parameters for initializing wallet - either by token ID or wallet ID. - * When using walletId, you can optionally pass the wallet instance to skip decryption. + * Parameters for initializing wallet by token ID. */ -export type InitWalletParams = - | { tokenId: string } - | { walletId: string; wallet?: IssuerSparkWallet }; +export type InitWalletParams = { tokenId: string }; /** * Parameters for creating a new token. - * Requires initWallet() to be called first. + * Automatically creates a new wallet if none is loaded. */ export type CreateTokenParams = { tokenName: string; From 2c95f7a1eaff8d92478cf9f9bae967176c6ba4f5 Mon Sep 17 00:00:00 2001 From: Jingles Date: Wed, 7 Jan 2026 01:18:13 +0800 Subject: [PATCH 25/30] fix build errors --- examples/developer-controlled-wallet/index.ts | 2 +- package.json | 1 + src/functions/crypto/encryption.test.ts | 38 +++++++++++++------ src/types/core/index.ts | 9 +++++ 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/examples/developer-controlled-wallet/index.ts b/examples/developer-controlled-wallet/index.ts index 24f046b..6c76146 100644 --- a/examples/developer-controlled-wallet/index.ts +++ b/examples/developer-controlled-wallet/index.ts @@ -1,4 +1,4 @@ -import { Web3Sdk } from "@meshsdk/web3-sdk"; +import { Web3Sdk } from "@utxos/sdk"; /** * Example: Developer-Controlled Wallet diff --git a/package.json b/package.json index 3fa55ea..3a68f20 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "@buildonspark/issuer-sdk": "^0.1.5", "@buildonspark/spark-sdk": "0.5.0", "@meshsdk/bitcoin": "1.9.0-beta.89", "@meshsdk/common": "1.9.0-beta.89", diff --git a/src/functions/crypto/encryption.test.ts b/src/functions/crypto/encryption.test.ts index 66ff248..c0646ef 100644 --- a/src/functions/crypto/encryption.test.ts +++ b/src/functions/crypto/encryption.test.ts @@ -1,3 +1,4 @@ +import { crypto } from "."; import { decryptWithCipher, decryptWithPrivateKey, @@ -9,21 +10,34 @@ import { const data = "solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution"; -describe("with cipher", () => { - const key = "01234567890123456789"; - - it("decrypt - 12 IV length", async () => { - const encryptedDataJSON = - '{"iv":"/bs1AzciZ1bDqT5W","ciphertext":"mh5pgH8ErqqH2KLLEBqqr8Pwm+mUuh9HhaAHslSD8ho6zk7mXccc9NUQAW8rb9UajCq8LYyANuiorjYD5N0hd2Lbe2n1x8AGRZrogyRKW6uhoFD1/FW6ofjgGP/kQRQSW2ZdJaDMbCxwYSdzxmaRunk6JRfybhfRU6kIxPMu41jhhRC3LbwZ+NnfBJFrg859hbuQgMQm8mqOUgOxcK8kKH54shOpGuLT4YBXhx33dZ//wT5VXrQ8kwIKttNk5h9MNKCacpRZSqU3pGlZ5oxucNEGos0IKTTXfbmwYx14uiERcXd32OP2"}'; +async function deriveKeyFromPassword(password: string): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveKey"], + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encoder.encode("static-salt-for-test"), + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); +} - const decrypted = await decryptWithCipher({ - encryptedDataJSON: encryptedDataJSON, - key, - }); +describe("with cipher", () => { + const keyString = "01234567890123456789"; - expect(data).toBe(decrypted); - }); it("encrypt and decrypt", async () => { + const key = await deriveKeyFromPassword(keyString); const encryptedDataJSON = await encryptWithCipher({ data, key, diff --git a/src/types/core/index.ts b/src/types/core/index.ts index 869ae86..71d72c4 100644 --- a/src/types/core/index.ts +++ b/src/types/core/index.ts @@ -36,6 +36,15 @@ export type Web3ProjectCardanoWallet = { stakeCredentialHash: string; }; +export type Web3ProjectWallet = { + id: string; + key: string; + tags: string[]; + projectId: string; + pubKeyHash?: string | null; + stakeCredentialHash?: string | null; +}; + export type Web3ProjectSparkWallet = { id: string; key: string; From c1f79787b735debfbc46a303e5f5cabd1c29e289 Mon Sep 17 00:00:00 2001 From: Jingles Date: Thu, 8 Jan 2026 12:02:33 +0800 Subject: [PATCH 26/30] refactor developer controlled wallet --- src/sdk/tokenization/cardano.ts | 10 +- src/sdk/tokenization/spark.ts | 113 +++++++++++------- .../wallet-developer-controlled/cardano.ts | 14 +-- src/sdk/wallet-developer-controlled/index.ts | 12 +- .../spark-issuer.ts | 14 +-- src/types/core/index.ts | 27 ----- src/types/core/multi-chain.ts | 2 - 7 files changed, 90 insertions(+), 102 deletions(-) diff --git a/src/sdk/tokenization/cardano.ts b/src/sdk/tokenization/cardano.ts index 8550e5d..b522712 100644 --- a/src/sdk/tokenization/cardano.ts +++ b/src/sdk/tokenization/cardano.ts @@ -1,6 +1,6 @@ import { Web3Sdk } from ".."; import { decryptWithPrivateKey } from "../../functions"; -import { Web3ProjectCardanoWallet } from "../../types"; +import { MultiChainWalletInfo } from "../../types"; import { MeshWallet } from "@meshsdk/wallet"; import { TokenizationTransaction, @@ -37,13 +37,13 @@ export type { export class TokenizationCardano { private readonly sdk: Web3Sdk; private wallet: MeshWallet | null = null; - private walletInfo: Web3ProjectCardanoWallet | null = null; + private walletInfo: MultiChainWalletInfo | null = null; constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; } - setWallet(wallet: MeshWallet, walletInfo: Web3ProjectCardanoWallet): void { + setWallet(wallet: MeshWallet, walletInfo: MultiChainWalletInfo): void { this.wallet = wallet; this.walletInfo = walletInfo; } @@ -66,7 +66,7 @@ export class TokenizationCardano { throw new Error("Failed to get Cardano wallet"); } - const walletInfo = data as Web3ProjectCardanoWallet; + const walletInfo = data as MultiChainWalletInfo; const mnemonic = await decryptWithPrivateKey({ privateKey: this.sdk.privateKey, @@ -293,7 +293,7 @@ export class TokenizationCardano { private async logTransaction(params: { tokenId: string; - walletInfo: Web3ProjectCardanoWallet; + walletInfo: MultiChainWalletInfo; type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; txHash?: string; amount?: string; diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index e9f98b4..6bbb054 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -1,6 +1,6 @@ import { Web3Sdk } from ".."; import { decryptWithPrivateKey } from "../../functions"; -import { Web3ProjectSparkWallet } from "../../types"; +import { MultiChainWalletInfo } from "../../types"; import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; import { v4 as uuidv4 } from "uuid"; import { @@ -70,7 +70,8 @@ export type { export class TokenizationSpark { private readonly sdk: Web3Sdk; private wallet: IssuerSparkWallet | null = null; - private walletInfo: Web3ProjectSparkWallet | null = null; + private walletInfo: MultiChainWalletInfo | null = null; + private walletNetwork: "MAINNET" | "REGTEST" = "MAINNET"; constructor({ sdk }: { sdk: Web3Sdk }) { this.sdk = sdk; @@ -105,7 +106,8 @@ export class TokenizationSpark { throw new Error("Failed to get Spark wallet"); } - const walletInfo = data as Web3ProjectSparkWallet; + const walletProject = data as MultiChainWalletInfo; + this.walletNetwork = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; if (this.sdk.privateKey === undefined) { throw new Error("Private key not found - required to decrypt wallet"); @@ -113,16 +115,19 @@ export class TokenizationSpark { const mnemonic = await decryptWithPrivateKey({ privateKey: this.sdk.privateKey, - encryptedDataJSON: walletInfo.key, + encryptedDataJSON: walletProject.key, }); const { wallet } = await IssuerSparkWallet.initialize({ mnemonicOrSeed: mnemonic, - options: { network: walletInfo.network }, + options: { + network: this.walletNetwork, + }, }); + console.log(13, wallet); this.wallet = wallet; - this.walletInfo = walletInfo; + this.walletInfo = walletProject; } /** @@ -144,6 +149,7 @@ export class TokenizationSpark { const normalizedTokenId = this.normalizeTokenId(params.tokenId); const policy = await this.getTokenizationPolicy(normalizedTokenId); await this.initWalletByWalletId(policy.walletId); + console.log(44); return policy; } @@ -154,7 +160,10 @@ export class TokenizationSpark { private normalizeTokenId(tokenId: string): string { const network = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; try { - const decoded = decodeBech32mTokenIdentifier(tokenId as Bech32mTokenIdentifier, network); + const decoded = decodeBech32mTokenIdentifier( + tokenId as Bech32mTokenIdentifier, + network, + ); return Buffer.from(decoded.tokenIdentifier).toString("hex"); } catch { return tokenId; @@ -194,22 +203,12 @@ export class TokenizationSpark { walletId: string; }> { if (!this.wallet || !this.walletInfo) { - const { info, sparkIssuerWallet } = await this.sdk.wallet.createWallet({ + const { info, cardanoWallet, sparkIssuerWallet } = await this.sdk.wallet.createWallet({ tags: ["tokenization"], }); - const networkParam = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; - this.walletInfo = { - id: info.id, - key: info.key, - tags: info.tags, - projectId: info.projectId, - publicKey: - networkParam === "MAINNET" - ? info.chains.spark?.mainnetPublicKey || "" - : info.chains.spark?.regtestPublicKey || "", - network: networkParam, - }; + this.walletNetwork = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; + this.walletInfo = info; this.wallet = sparkIssuerWallet; } @@ -222,11 +221,12 @@ export class TokenizationSpark { }); const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); - const network = this.walletInfo.network === "MAINNET" ? "MAINNET" : "REGTEST"; + const tokenIdHex = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); const tokenId = encodeBech32mTokenIdentifier({ tokenIdentifier: tokenMetadata.rawTokenIdentifier, - network, + network: this.walletNetwork, }); // Save tokenization policy to database @@ -236,7 +236,7 @@ export class TokenizationSpark { projectId: this.sdk.projectId, walletId: this.walletInfo.id, chain: "spark", - network: this.walletInfo.network.toLowerCase(), + network: this.walletNetwork.toLowerCase(), }); // Log the create transaction @@ -268,7 +268,9 @@ export class TokenizationSpark { const txHash = await this.wallet.mintTokens(params.amount); const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); await this.logTransaction({ txId: txHash, @@ -321,11 +323,12 @@ export class TokenizationSpark { } const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); - const network = this.walletInfo.network === "MAINNET" ? "MAINNET" : "REGTEST"; + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); const bech32mTokenId = encodeBech32mTokenIdentifier({ tokenIdentifier: tokenMetadata.rawTokenIdentifier, - network, + network: this.walletNetwork, }); const txHash = await this.wallet.transferTokens({ tokenIdentifier: bech32mTokenId, @@ -362,7 +365,9 @@ export class TokenizationSpark { const txHash = await this.wallet.burnTokens(params.amount); const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); await this.logTransaction({ txId: txHash, @@ -390,13 +395,17 @@ export class TokenizationSpark { const result = await this.wallet.freezeTokens(params.address); const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); try { const publicKeyHash = extractIdentityPublicKey(params.address); if (!publicKeyHash) { - throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); + throw new Error( + `Failed to extract public key hash from Spark address: ${params.address}`, + ); } // Update frozen addresses table @@ -405,7 +414,7 @@ export class TokenizationSpark { projectId: this.sdk.projectId, projectWalletId: this.walletInfo.id, chain: "spark", - network: this.walletInfo.network.toLowerCase(), + network: this.walletNetwork.toLowerCase(), publicKeyHash, isFrozen: true, freezeReason: params.freezeReason || "Frozen by issuer", @@ -422,7 +431,6 @@ export class TokenizationSpark { toAddress: params.address, amount: result.impactedTokenAmount.toString(), }); - } catch (saveError) { console.warn("Failed to save freeze operation:", saveError); } @@ -440,7 +448,9 @@ export class TokenizationSpark { * @param params - Unfreeze parameters * @returns Unfreeze operation results */ - async unfreezeTokens(params: UnfreezeTokensParams): Promise { + async unfreezeTokens( + params: UnfreezeTokensParams, + ): Promise { if (!this.wallet || !this.walletInfo) { throw new Error("No wallet loaded. Call initWallet(walletId) first."); } @@ -448,13 +458,17 @@ export class TokenizationSpark { const result = await this.wallet.unfreezeTokens(params.address); const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); try { const publicKeyHash = extractIdentityPublicKey(params.address); if (!publicKeyHash) { - throw new Error(`Failed to extract public key hash from Spark address: ${params.address}`); + throw new Error( + `Failed to extract public key hash from Spark address: ${params.address}`, + ); } // Update frozen addresses table @@ -503,7 +517,9 @@ export class TokenizationSpark { const { includeUnfrozen = false, page = 1, limit = 15 } = params || {}; const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); const { data, status } = await this.sdk.axiosInstance.get( `api/tokenization/frozen-addresses`, @@ -515,7 +531,7 @@ export class TokenizationSpark { page, limit, }, - } + }, ); if (status === 200) { @@ -546,7 +562,9 @@ export class TokenizationSpark { const { type, page = 1, limit = 50 } = params || {}; const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); - const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString("hex"); + const tokenId = Buffer.from(tokenMetadata.rawTokenIdentifier).toString( + "hex", + ); const { data, status } = await this.sdk.axiosInstance.get( `api/tokenization/transactions`, @@ -558,7 +576,7 @@ export class TokenizationSpark { page, limit, }, - } + }, ); if (status === 200) { @@ -577,7 +595,9 @@ export class TokenizationSpark { * @param params - Optional filter and pagination parameters * @returns List of tokenization policies with pagination info */ - async getTokenizationPolicies(params?: ListTokenizationPoliciesParams): Promise<{ + async getTokenizationPolicies( + params?: ListTokenizationPoliciesParams, + ): Promise<{ tokens: TokenizationPolicy[]; pagination: TokenizationPaginationInfo; }> { @@ -592,7 +612,7 @@ export class TokenizationSpark { page, limit, }, - } + }, ); if (status === 200) { @@ -612,7 +632,10 @@ export class TokenizationSpark { * @returns The tokenization policy */ async getTokenizationPolicy(tokenId: string): Promise { - const { tokens } = await this.getTokenizationPolicies({ tokenId, limit: 1 }); + const { tokens } = await this.getTokenizationPolicies({ + tokenId, + limit: 1, + }); const policy = tokens[0]; if (!policy) { @@ -628,7 +651,7 @@ export class TokenizationSpark { private async logTransaction(params: { txId: string; tokenId: string; - walletInfo: Web3ProjectSparkWallet; + walletInfo: MultiChainWalletInfo; type: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; amount?: string; fromAddress?: string; @@ -643,7 +666,7 @@ export class TokenizationSpark { projectWalletId: params.walletInfo.id, type: params.type, chain: "spark", - network: params.walletInfo.network.toLowerCase(), + network: this.walletNetwork.toLowerCase(), amount: params.amount, fromAddress: params.fromAddress, toAddress: params.toAddress, diff --git a/src/sdk/wallet-developer-controlled/cardano.ts b/src/sdk/wallet-developer-controlled/cardano.ts index de05080..878afb2 100644 --- a/src/sdk/wallet-developer-controlled/cardano.ts +++ b/src/sdk/wallet-developer-controlled/cardano.ts @@ -1,7 +1,7 @@ import { Web3Sdk } from ".."; import { MeshWallet } from "@meshsdk/wallet"; import { decryptWithPrivateKey } from "../../functions"; -import { Web3ProjectCardanoWallet, TokenCreationParams } from "../../types"; +import { MultiChainWalletInfo, TokenCreationParams } from "../../types"; /** * CardanoWalletDeveloperControlled - Manages Cardano-specific developer-controlled wallets. @@ -43,13 +43,13 @@ export class CardanoWalletDeveloperControlled { * console.log(`Found ${wallets.length} Cardano wallets`); * ``` */ - async getWallets(): Promise { + async getWallets(): Promise { const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/cardano`, ); if (status === 200) { - return data as Web3ProjectCardanoWallet[]; + return data as MultiChainWalletInfo[]; } throw new Error("Failed to get Cardano wallets"); @@ -73,7 +73,7 @@ export class CardanoWalletDeveloperControlled { walletId: string, decryptKey = false, ): Promise<{ - info: Web3ProjectCardanoWallet; + info: MultiChainWalletInfo; wallet: MeshWallet; }> { if (this.sdk.privateKey === undefined) { @@ -85,7 +85,7 @@ export class CardanoWalletDeveloperControlled { ); if (status === 200) { - const web3Wallet = data as Web3ProjectCardanoWallet; + const web3Wallet = data as MultiChainWalletInfo; const mnemonic = await decryptWithPrivateKey({ privateKey: this.sdk.privateKey, @@ -125,7 +125,7 @@ export class CardanoWalletDeveloperControlled { * const wallets = await sdk.wallet.cardano.getWalletsByTag("treasury"); * ``` */ - async getWalletsByTag(tag: string): Promise { + async getWalletsByTag(tag: string): Promise { if (this.sdk.privateKey === undefined) { throw new Error("Private key not found"); } @@ -135,7 +135,7 @@ export class CardanoWalletDeveloperControlled { ); if (status === 200) { - return data as Web3ProjectCardanoWallet[]; + return data as MultiChainWalletInfo[]; } throw new Error("Failed to get Cardano wallets by tag"); diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index da9cde6..c43aa9d 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -122,17 +122,15 @@ export class WalletDeveloperControlled { const sparkWallet = networkId === 1 ? sparkMainnetWallet : sparkRegtestWallet; - const walletData = { + const walletData: MultiChainWalletInfo = { id: walletId, projectId: this.sdk.projectId, tags: options.tags || [], key: encryptedKey, - networkId, chains: { cardano: { pubKeyHash, stakeCredentialHash }, spark: { mainnetPublicKey, regtestPublicKey }, }, - createdAt: new Date().toISOString(), }; const { status } = await this.sdk.axiosInstance.post( @@ -142,7 +140,7 @@ export class WalletDeveloperControlled { if (status === 200) { return { - info: walletData as MultiChainWalletInfo, + info: walletData, sparkIssuerWallet: sparkWallet, cardanoWallet: cardanoWallet, }; @@ -259,11 +257,7 @@ export class WalletDeveloperControlled { instance.cardanoWallet = cardanoWallet; } - if ( - (chain === "spark" || !chain) && - walletInfo.chains.spark && - mnemonic - ) { + if ((chain === "spark" || !chain) && walletInfo.chains.spark && mnemonic) { const sparkNetwork = networkId === 1 ? "MAINNET" : "REGTEST"; const { wallet: sparkWallet } = await IssuerSparkWallet.initialize({ mnemonicOrSeed: mnemonic, diff --git a/src/sdk/wallet-developer-controlled/spark-issuer.ts b/src/sdk/wallet-developer-controlled/spark-issuer.ts index ab89c0e..a1be8aa 100644 --- a/src/sdk/wallet-developer-controlled/spark-issuer.ts +++ b/src/sdk/wallet-developer-controlled/spark-issuer.ts @@ -1,5 +1,5 @@ import { Web3Sdk } from ".."; -import { Web3ProjectSparkWallet } from "../../types"; +import { MultiChainWalletInfo } from "../../types"; /** * SparkIssuerWalletDeveloperControlled - Developer-controlled Spark issuer wallet management @@ -37,14 +37,14 @@ export class SparkIssuerWalletDeveloperControlled { * const walletInfo = await sdk.wallet.sparkIssuer.get("existing-wallet-id"); * ``` */ - async getWallet(walletId: string): Promise { + async getWallet(walletId: string): Promise { const networkParam = this.sdk.network === "mainnet" ? "mainnet" : "regtest"; const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/${walletId}?chain=spark&network=${networkParam}`, ); if (status === 200) { - return data as Web3ProjectSparkWallet; + return data as MultiChainWalletInfo; } throw new Error("Failed to get Spark wallet"); @@ -63,13 +63,13 @@ export class SparkIssuerWalletDeveloperControlled { * wallets.forEach(w => console.log(`- ${w.id}: tags=[${w.tags.join(', ')}]`)); * ``` */ - async list(): Promise { + async list(): Promise { const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/spark`, ); if (status === 200) { - return data as Web3ProjectSparkWallet[]; + return data as MultiChainWalletInfo[]; } throw new Error("Failed to get Spark wallets"); @@ -86,13 +86,13 @@ export class SparkIssuerWalletDeveloperControlled { * const wallets = await sdk.wallet.sparkIssuer.getByTag("tokenization"); * ``` */ - async getByTag(tag: string): Promise { + async getByTag(tag: string): Promise { const { data, status } = await this.sdk.axiosInstance.get( `api/project-wallet/${this.sdk.projectId}/spark/tag/${tag}`, ); if (status === 200) { - return data as Web3ProjectSparkWallet[]; + return data as MultiChainWalletInfo[]; } throw new Error("Failed to get Spark wallets by tag"); diff --git a/src/types/core/index.ts b/src/types/core/index.ts index 71d72c4..6cd20ef 100644 --- a/src/types/core/index.ts +++ b/src/types/core/index.ts @@ -27,33 +27,6 @@ export type Web3ProjectBranding = { appleEnabled?: boolean; }; -export type Web3ProjectCardanoWallet = { - id: string; - key: string; - tags: string[]; - projectId: string; - pubKeyHash: string; - stakeCredentialHash: string; -}; - -export type Web3ProjectWallet = { - id: string; - key: string; - tags: string[]; - projectId: string; - pubKeyHash?: string | null; - stakeCredentialHash?: string | null; -}; - -export type Web3ProjectSparkWallet = { - id: string; - key: string; - tags: string[]; - projectId: string; - publicKey: string; - network: "MAINNET" | "REGTEST"; -}; - export type Web3JWTBody = { /** User's ID */ sub: string; diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index b1555cf..faf57f5 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -22,7 +22,6 @@ export interface MultiChainWalletInfo { projectId: string; tags: string[]; key: string; - networkId: NetworkId; chains: { cardano?: { pubKeyHash: string; @@ -33,7 +32,6 @@ export interface MultiChainWalletInfo { regtestPublicKey: string; }; }; - createdAt: string; } /** From 6deb0847fb199553e942a3d974179f0f6d40616b Mon Sep 17 00:00:00 2001 From: Jingles Date: Fri, 9 Jan 2026 00:16:20 +0800 Subject: [PATCH 27/30] refactor spark token --- src/sdk/tokenization/spark.ts | 59 ++++++++++++++++----------------- src/types/spark/tokenization.ts | 18 +++++----- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/sdk/tokenization/spark.ts b/src/sdk/tokenization/spark.ts index 6bbb054..219cc88 100644 --- a/src/sdk/tokenization/spark.ts +++ b/src/sdk/tokenization/spark.ts @@ -15,7 +15,7 @@ import { TokenizationFrozenAddress, TokenizationPaginationInfo, TokenizationPolicy, - InitWalletParams, + InitTokenizationParams, CreateTokenParams, MintTokensParams, TransferTokensParams, @@ -28,7 +28,7 @@ import { } from "../../types/spark/tokenization"; export type { - InitWalletParams, + InitTokenizationParams, CreateTokenParams, MintTokensParams, TransferTokensParams, @@ -60,7 +60,7 @@ export type { * }); * * // Load existing token by token ID - * const policy = await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); + * const policy = await sdk.tokenization.spark.initTokenization({ tokenId: "btknrt1..." }); * * // Perform token operations * await sdk.tokenization.spark.mintTokens({ amount: BigInt("1000000") }); @@ -124,14 +124,13 @@ export class TokenizationSpark { network: this.walletNetwork, }, }); - console.log(13, wallet); this.wallet = wallet; this.walletInfo = walletProject; } /** - * Initializes the tokenization wallet by token ID. + * Initializes the tokenization by token ID. * * @param params - { tokenId } - the token ID to load * @returns The tokenization policy @@ -139,17 +138,16 @@ export class TokenizationSpark { * @example * ```typescript * // Load existing token by token ID - * const policy = await sdk.tokenization.spark.initWallet({ tokenId: "btknrt1..." }); + * const policy = await sdk.tokenization.spark.initTokenization({ tokenId: "btknrt1..." }); * * // Then perform operations * await sdk.tokenization.spark.mintTokens({ amount: BigInt(1000) }); * ``` */ - async initWallet(params: InitWalletParams): Promise { + async initTokenization(params: InitTokenizationParams): Promise { const normalizedTokenId = this.normalizeTokenId(params.tokenId); const policy = await this.getTokenizationPolicy(normalizedTokenId); await this.initWalletByWalletId(policy.walletId); - console.log(44); return policy; } @@ -188,7 +186,7 @@ export class TokenizationSpark { * }); * * // Or load existing wallet first - * await sdk.tokenization.spark.initWallet({ walletId: "existing-wallet-id" }); + * await sdk.tokenization.spark.initTokenization({ tokenId: "existing-token-id" }); * const { tokenId } = await sdk.tokenization.spark.createToken({ * tokenName: "MyToken", * tokenTicker: "MTK", @@ -203,11 +201,12 @@ export class TokenizationSpark { walletId: string; }> { if (!this.wallet || !this.walletInfo) { - const { info, cardanoWallet, sparkIssuerWallet } = await this.sdk.wallet.createWallet({ - tags: ["tokenization"], + const { info, sparkIssuerWallet } = await this.sdk.wallet.createWallet({ + tags: ["tokenization", "spark"], }); - this.walletNetwork = this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; + this.walletNetwork = + this.sdk.network === "mainnet" ? "MAINNET" : "REGTEST"; this.walletInfo = info; this.wallet = sparkIssuerWallet; } @@ -255,14 +254,14 @@ export class TokenizationSpark { /** * Mints tokens from the issuer wallet. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Mint parameters including amount * @returns Transaction ID of the mint operation */ async mintTokens(params: MintTokensParams): Promise { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const txHash = await this.wallet.mintTokens(params.amount); @@ -285,13 +284,13 @@ export class TokenizationSpark { /** * Gets the token balance for an issuer wallet. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @returns Balance information */ async getTokenBalance(): Promise<{ balance: string }> { if (!this.wallet) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const result = await this.wallet.getIssuerTokenBalance(); return { balance: result.balance.toString() }; @@ -299,27 +298,27 @@ export class TokenizationSpark { /** * Gets metadata for the token created by an issuer wallet. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @returns Token metadata */ async getTokenMetadata() { if (!this.wallet) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } return await this.wallet.getIssuerTokenMetadata(); } /** * Transfers tokens from the issuer wallet to another address. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Transfer parameters * @returns Transaction ID of the transfer */ async transferTokens(params: TransferTokensParams): Promise { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const tokenMetadata = await this.wallet.getIssuerTokenMetadata(); @@ -352,14 +351,14 @@ export class TokenizationSpark { /** * Burns tokens permanently from circulation. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Burn parameters * @returns Transaction ID of the burn operation */ async burnTokens(params: BurnTokensParams): Promise { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const txHash = await this.wallet.burnTokens(params.amount); @@ -382,14 +381,14 @@ export class TokenizationSpark { /** * Freezes tokens at a specific Spark address for compliance purposes. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Freeze parameters * @returns Freeze operation results */ async freezeTokens(params: FreezeTokensParams): Promise { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const result = await this.wallet.freezeTokens(params.address); @@ -443,7 +442,7 @@ export class TokenizationSpark { /** * Unfreezes tokens at a specific Spark address. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Unfreeze parameters * @returns Unfreeze operation results @@ -452,7 +451,7 @@ export class TokenizationSpark { params: UnfreezeTokensParams, ): Promise { if (!this.wallet || !this.walletInfo) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const result = await this.wallet.unfreezeTokens(params.address); @@ -501,7 +500,7 @@ export class TokenizationSpark { /** * Lists frozen addresses for a token from the database. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Query parameters including pagination * @returns List of frozen addresses with pagination info @@ -511,7 +510,7 @@ export class TokenizationSpark { pagination: TokenizationPaginationInfo; }> { if (!this.wallet) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const { includeUnfrozen = false, page = 1, limit = 15 } = params || {}; @@ -546,7 +545,7 @@ export class TokenizationSpark { /** * Lists token transactions from the database. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. * * @param params - Query parameters including type filter and pagination * @returns List of transactions with pagination info @@ -556,7 +555,7 @@ export class TokenizationSpark { pagination: TokenizationPaginationInfo; }> { if (!this.wallet) { - throw new Error("No wallet loaded. Call initWallet(walletId) first."); + throw new Error("No wallet loaded. Call initTokenization(tokenId) first."); } const { type, page = 1, limit = 50 } = params || {}; diff --git a/src/types/spark/tokenization.ts b/src/types/spark/tokenization.ts index c0841af..da4f217 100644 --- a/src/types/spark/tokenization.ts +++ b/src/types/spark/tokenization.ts @@ -56,9 +56,9 @@ export type TokenizationPolicy = { }; /** - * Parameters for initializing wallet by token ID. + * Parameters for initializing tokenization by token ID. */ -export type InitWalletParams = { tokenId: string }; +export type InitTokenizationParams = { tokenId: string }; /** * Parameters for creating a new token. @@ -74,7 +74,7 @@ export type CreateTokenParams = { /** * Parameters for minting tokens. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type MintTokensParams = { amount: bigint; @@ -82,7 +82,7 @@ export type MintTokensParams = { /** * Parameters for transferring tokens. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type TransferTokensParams = { amount: bigint; @@ -91,7 +91,7 @@ export type TransferTokensParams = { /** * Parameters for burning tokens. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type BurnTokensParams = { amount: bigint; @@ -99,7 +99,7 @@ export type BurnTokensParams = { /** * Parameters for freezing tokens. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type FreezeTokensParams = { address: string; @@ -108,7 +108,7 @@ export type FreezeTokensParams = { /** * Parameters for unfreezing tokens. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type UnfreezeTokensParams = { address: string; @@ -116,7 +116,7 @@ export type UnfreezeTokensParams = { /** * Parameters for listing transactions. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type ListTransactionsParams = { type?: "create" | "mint" | "burn" | "transfer" | "freeze" | "unfreeze"; @@ -126,7 +126,7 @@ export type ListTransactionsParams = { /** * Parameters for listing frozen addresses. - * Requires initWallet() to be called first. + * Requires initTokenization() to be called first. */ export type ListFrozenAddressesParams = { includeUnfrozen?: boolean; From 07fa8a324afc47f259eea1d610a9af9d3bbc400c Mon Sep 17 00:00:00 2001 From: Jingles Date: Fri, 9 Jan 2026 12:57:04 +0800 Subject: [PATCH 28/30] added a lot of tests --- package.json | 3 +- src/functions/client/derive-wallet.test.ts | 287 ++++++++ src/functions/client/generate-wallet.test.ts | 301 +++++++++ src/functions/client/recovery.test.ts | 385 +++++++++++ src/functions/convertors.test.ts | 146 ++++ src/functions/crypto/encryption.test.ts | 236 ++++++- src/functions/crypto/hash.test.ts | 102 +++ .../combine-shards-build-wallet.test.ts | 267 ++++++++ .../key-shard/shamir-secret-sharing.test.ts | 226 +++++++ .../key-shard/spilt-key-into-shards.test.ts | 165 +++++ .../wallet-developer-controlled/index.test.ts | 633 ++++++++++++++++++ src/types/core/multi-chain.ts | 26 +- 12 files changed, 2757 insertions(+), 20 deletions(-) create mode 100644 src/functions/client/derive-wallet.test.ts create mode 100644 src/functions/client/generate-wallet.test.ts create mode 100644 src/functions/client/recovery.test.ts create mode 100644 src/functions/convertors.test.ts create mode 100644 src/functions/crypto/hash.test.ts create mode 100644 src/functions/key-shard/combine-shards-build-wallet.test.ts create mode 100644 src/functions/key-shard/shamir-secret-sharing.test.ts create mode 100644 src/functions/key-shard/spilt-key-into-shards.test.ts create mode 100644 src/sdk/wallet-developer-controlled/index.test.ts diff --git a/package.json b/package.json index 3a68f20..9e0ba7b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "scripts": { "build:sdk": "tsup src/index.ts --format esm,cjs --dts", "dev": "tsup src/index.ts --format esm,cjs --watch --dts", - "test": "jest" + "test": "npx jest" }, "devDependencies": { "@types/base32-encoding": "^1.0.2", @@ -41,6 +41,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "@utxos/api-contracts": "*", "@buildonspark/issuer-sdk": "^0.1.5", "@buildonspark/spark-sdk": "0.5.0", "@meshsdk/bitcoin": "1.9.0-beta.89", diff --git a/src/functions/client/derive-wallet.test.ts b/src/functions/client/derive-wallet.test.ts new file mode 100644 index 0000000..f7c2b89 --- /dev/null +++ b/src/functions/client/derive-wallet.test.ts @@ -0,0 +1,287 @@ +import { crypto } from "../crypto"; +import { encryptWithCipher } from "../crypto"; +import { spiltKeyIntoShards } from "../key-shard"; + +// Mock external wallet SDKs +jest.mock("@meshsdk/wallet", () => ({ + MeshWallet: jest.fn().mockImplementation(() => ({ + init: jest.fn().mockResolvedValue(undefined), + getUsedAddresses: jest.fn().mockResolvedValue(["addr_test1..."]), + })), +})); + +jest.mock("@meshsdk/bitcoin", () => ({ + EmbeddedWallet: jest.fn().mockImplementation(() => ({ + getAddress: jest.fn().mockResolvedValue("bc1q..."), + })), +})); + +jest.mock("@buildonspark/spark-sdk", () => ({ + SparkWallet: { + initialize: jest.fn().mockResolvedValue({ + wallet: { + getAddress: jest.fn().mockResolvedValue("spark1..."), + }, + }), + }, +})); + +// Import after mocks +import { clientDeriveWallet } from "./derive-wallet"; +import { MeshWallet } from "@meshsdk/wallet"; +import { EmbeddedWallet } from "@meshsdk/bitcoin"; +import { SparkWallet } from "@buildonspark/spark-sdk"; + +async function deriveKeyFromPassword(password: string): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveKey"], + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encoder.encode("static-salt-for-test"), + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); +} + +describe("clientDeriveWallet", () => { + const testMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("derives wallet from encrypted device shard and custodial shard", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + // Encrypt the device shard (shard 1) + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + // Custodial shard is shard 2 (auth shard) + const custodialShard = shards[1]!; + + const result = await clientDeriveWallet( + encryptedDeviceShard, + deviceKey, + custodialShard, + 0, // testnet + ); + + expect(result).toHaveProperty("bitcoinWallet"); + expect(result).toHaveProperty("cardanoWallet"); + expect(result).toHaveProperty("sparkWallet"); + expect(result).toHaveProperty("key"); + }); + + it("returns the reconstructed mnemonic as key", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + const result = await clientDeriveWallet( + encryptedDeviceShard, + deviceKey, + shards[1]!, + 0, + ); + + expect(result.key).toBe(testMnemonic); + }); + + it("creates wallets with testnet configuration when networkId is 0", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + await clientDeriveWallet(encryptedDeviceShard, deviceKey, shards[1]!, 0); + + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ network: "Testnet" }), + ); + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ networkId: 0 }), + ); + expect(SparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ network: "REGTEST" }), + }), + ); + }); + + it("creates wallets with mainnet configuration when networkId is 1", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + await clientDeriveWallet(encryptedDeviceShard, deviceKey, shards[1]!, 1); + + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ network: "Mainnet" }), + ); + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ networkId: 1 }), + ); + expect(SparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ network: "MAINNET" }), + }), + ); + }); + + it("passes bitcoinProvider when provided", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + const mockProvider = { getUtxos: jest.fn() }; + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + await clientDeriveWallet( + encryptedDeviceShard, + deviceKey, + shards[1]!, + 0, + mockProvider as any, + ); + + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ provider: mockProvider }), + ); + }); + + it("fails with wrong decryption key", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const correctKey = await deriveKeyFromPassword("correct-password"); + const wrongKey = await deriveKeyFromPassword("wrong-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: correctKey, + }); + + await expect( + clientDeriveWallet(encryptedDeviceShard, wrongKey, shards[1]!, 0), + ).rejects.toThrow(); + }); + + it("fails with corrupted encrypted shard", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + // Corrupt the encrypted data + const parsed = JSON.parse(encryptedDeviceShard); + parsed.ciphertext = "corrupted" + parsed.ciphertext; + + await expect( + clientDeriveWallet(JSON.stringify(parsed), deviceKey, shards[1]!, 0), + ).rejects.toThrow(); + }); + + it("fails with invalid JSON in encrypted shard", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + await expect( + clientDeriveWallet("not-valid-json", deviceKey, shards[1]!, 0), + ).rejects.toThrow(); + }); + + it("works with shards from different positions", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + // Use shard 2 as device shard and shard 3 as custodial + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[1]!, + key: deviceKey, + }); + + const result = await clientDeriveWallet( + encryptedDeviceShard, + deviceKey, + shards[2]!, + 0, + ); + + expect(result.key).toBe(testMnemonic); + }); + + it("initializes cardano wallet", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const deviceKey = await deriveKeyFromPassword("device-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + const result = await clientDeriveWallet( + encryptedDeviceShard, + deviceKey, + shards[1]!, + 0, + ); + + expect(result.cardanoWallet.init).toHaveBeenCalled(); + }); +}); + +describe("clientDeriveWallet with 24-word mnemonic", () => { + const mnemonic24 = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + + it("derives wallet from 24-word mnemonic shards", async () => { + const shards = await spiltKeyIntoShards(mnemonic24); + const deviceKey = await deriveKeyFromPassword("device-password"); + + const encryptedDeviceShard = await encryptWithCipher({ + data: shards[0]!, + key: deviceKey, + }); + + const result = await clientDeriveWallet( + encryptedDeviceShard, + deviceKey, + shards[1]!, + 0, + ); + + expect(result.key).toBe(mnemonic24); + expect(result.key.split(" ").length).toBe(24); + }); +}); diff --git a/src/functions/client/generate-wallet.test.ts b/src/functions/client/generate-wallet.test.ts new file mode 100644 index 0000000..6fde73d --- /dev/null +++ b/src/functions/client/generate-wallet.test.ts @@ -0,0 +1,301 @@ +import { crypto } from "../crypto"; + +// Mock external wallet SDKs +jest.mock("@meshsdk/wallet", () => ({ + MeshWallet: jest.fn().mockImplementation(() => ({ + init: jest.fn().mockResolvedValue(undefined), + getAddresses: jest.fn().mockResolvedValue({ + baseAddressBech32: + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + }), + })), +})); + +jest.mock("@meshsdk/common", () => ({ + generateMnemonic: jest.fn().mockResolvedValue( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + ), +})); + +jest.mock("@meshsdk/core-cst", () => ({ + deserializeBech32Address: jest.fn().mockReturnValue({ + pubKeyHash: "mock-pub-key-hash", + stakeCredentialHash: "mock-stake-credential-hash", + }), +})); + +jest.mock("@meshsdk/bitcoin", () => ({ + EmbeddedWallet: jest.fn().mockImplementation(() => ({ + getPublicKey: jest.fn().mockReturnValue("mock-bitcoin-pub-key-hash"), + getAddress: jest.fn().mockResolvedValue("bc1q..."), + })), +})); + +jest.mock("@buildonspark/spark-sdk", () => ({ + SparkWallet: { + initialize: jest.fn().mockResolvedValue({ + wallet: { + getIdentityPublicKey: jest.fn().mockResolvedValue("mock-spark-pub-key"), + getStaticDepositAddress: jest + .fn() + .mockResolvedValue("mock-spark-deposit-address"), + }, + }), + }, +})); + +// Import after mocks +import { clientGenerateWallet } from "./generate-wallet"; +import { generateMnemonic } from "@meshsdk/common"; +import { MeshWallet } from "@meshsdk/wallet"; +import { EmbeddedWallet } from "@meshsdk/bitcoin"; +import { SparkWallet } from "@buildonspark/spark-sdk"; +import { decryptWithCipher } from "../crypto"; +import { hexToBytes, bytesToString } from "../convertors"; +import { shamirCombine } from "../key-shard"; + +async function deriveKeyFromPassword(password: string): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveKey"], + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encoder.encode("static-salt-for-test"), + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); +} + +describe("clientGenerateWallet", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("generates a wallet and returns all required fields", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(result).toHaveProperty("encryptedDeviceShard"); + expect(result).toHaveProperty("authShard"); + expect(result).toHaveProperty("encryptedRecoveryShard"); + expect(result).toHaveProperty("bitcoinMainnetPubKeyHash"); + expect(result).toHaveProperty("bitcoinTestnetPubKeyHash"); + expect(result).toHaveProperty("cardanoPubKeyHash"); + expect(result).toHaveProperty("cardanoStakeCredentialHash"); + expect(result).toHaveProperty("sparkMainnetPubKeyHash"); + expect(result).toHaveProperty("sparkRegtestPubKeyHash"); + expect(result).toHaveProperty("sparkMainnetStaticDepositAddress"); + expect(result).toHaveProperty("sparkRegtestStaticDepositAddress"); + }); + + it("calls generateMnemonic with 256 bits", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + await clientGenerateWallet(deviceKey, recoveryKey); + + expect(generateMnemonic).toHaveBeenCalledWith(256); + }); + + it("returns encrypted device shard as JSON string", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(typeof result.encryptedDeviceShard).toBe("string"); + const parsed = JSON.parse(result.encryptedDeviceShard); + expect(parsed).toHaveProperty("iv"); + expect(parsed).toHaveProperty("ciphertext"); + }); + + it("returns encrypted recovery shard as JSON string", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(typeof result.encryptedRecoveryShard).toBe("string"); + const parsed = JSON.parse(result.encryptedRecoveryShard); + expect(parsed).toHaveProperty("iv"); + expect(parsed).toHaveProperty("ciphertext"); + }); + + it("returns authShard as hex string", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(typeof result.authShard).toBe("string"); + expect(result.authShard).toMatch(/^[0-9a-f]+$/); + }); + + it("device shard can be decrypted with device key", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + const decrypted = await decryptWithCipher({ + encryptedDataJSON: result.encryptedDeviceShard, + key: deviceKey, + }); + + expect(typeof decrypted).toBe("string"); + expect(decrypted).toMatch(/^[0-9a-f]+$/); // Should be hex-encoded shard + }); + + it("recovery shard can be decrypted with recovery key", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + const decrypted = await decryptWithCipher({ + encryptedDataJSON: result.encryptedRecoveryShard, + key: recoveryKey, + }); + + expect(typeof decrypted).toBe("string"); + expect(decrypted).toMatch(/^[0-9a-f]+$/); // Should be hex-encoded shard + }); + + it("device shard cannot be decrypted with wrong key", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const wrongKey = await deriveKeyFromPassword("wrong-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + await expect( + decryptWithCipher({ + encryptedDataJSON: result.encryptedDeviceShard, + key: wrongKey, + }), + ).rejects.toThrow(); + }); + + it("creates EmbeddedWallet for both testnet and mainnet", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + await clientGenerateWallet(deviceKey, recoveryKey); + + expect(EmbeddedWallet).toHaveBeenCalledTimes(2); + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ network: "Testnet" }), + ); + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ network: "Mainnet" }), + ); + }); + + it("creates MeshWallet with networkId 1", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + await clientGenerateWallet(deviceKey, recoveryKey); + + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ networkId: 1 }), + ); + }); + + it("initializes SparkWallet for both mainnet and regtest", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + await clientGenerateWallet(deviceKey, recoveryKey); + + expect(SparkWallet.initialize).toHaveBeenCalledTimes(2); + expect(SparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ network: "MAINNET" }), + }), + ); + expect(SparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ network: "REGTEST" }), + }), + ); + }); + + it("returns bitcoin pub key hashes", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(result.bitcoinMainnetPubKeyHash).toBe("mock-bitcoin-pub-key-hash"); + expect(result.bitcoinTestnetPubKeyHash).toBe("mock-bitcoin-pub-key-hash"); + }); + + it("returns cardano key hashes", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(result.cardanoPubKeyHash).toBe("mock-pub-key-hash"); + expect(result.cardanoStakeCredentialHash).toBe( + "mock-stake-credential-hash", + ); + }); + + it("returns spark pub key hashes and deposit addresses", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + expect(result.sparkMainnetPubKeyHash).toBe("mock-spark-pub-key"); + expect(result.sparkRegtestPubKeyHash).toBe("mock-spark-pub-key"); + expect(result.sparkMainnetStaticDepositAddress).toBe( + "mock-spark-deposit-address", + ); + expect(result.sparkRegtestStaticDepositAddress).toBe( + "mock-spark-deposit-address", + ); + }); + + it("2 of 3 shards can reconstruct the mnemonic", async () => { + const deviceKey = await deriveKeyFromPassword("device-password"); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + + const result = await clientGenerateWallet(deviceKey, recoveryKey); + + // Decrypt device shard (shard 1) + const shard1 = await decryptWithCipher({ + encryptedDataJSON: result.encryptedDeviceShard, + key: deviceKey, + }); + + // Auth shard is shard 2 (unencrypted hex) + const shard2 = result.authShard; + + // Combine shards 1 and 2 + const share1 = hexToBytes(shard1); + const share2 = hexToBytes(shard2); + const reconstructed = await shamirCombine([share1, share2]); + const mnemonic = bytesToString(reconstructed); + + // Should match the mock mnemonic + expect(mnemonic).toBe( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + ); + }); +}); diff --git a/src/functions/client/recovery.test.ts b/src/functions/client/recovery.test.ts new file mode 100644 index 0000000..bdbdb8f --- /dev/null +++ b/src/functions/client/recovery.test.ts @@ -0,0 +1,385 @@ +import { crypto } from "../crypto"; +import { encryptWithCipher, decryptWithCipher } from "../crypto"; +import { spiltKeyIntoShards } from "../key-shard"; +import { hexToBytes, bytesToString } from "../convertors"; +import { shamirCombine } from "../key-shard"; + +// Mock external wallet SDKs +jest.mock("@meshsdk/wallet", () => ({ + MeshWallet: jest.fn().mockImplementation(() => ({ + init: jest.fn().mockResolvedValue(undefined), + })), +})); + +jest.mock("@meshsdk/bitcoin", () => ({ + EmbeddedWallet: jest.fn().mockImplementation(() => ({})), +})); + +jest.mock("@buildonspark/spark-sdk", () => ({ + SparkWallet: { + initialize: jest.fn().mockResolvedValue({ + wallet: {}, + }), + }, +})); + +// Import after mocks +import { clientRecovery } from "./recovery"; + +async function deriveKeyFromPassword(password: string): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveKey"], + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encoder.encode("static-salt-for-test"), + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); +} + +describe("clientRecovery", () => { + const testMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("recovers wallet from auth shard and encrypted recovery shard", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + // Auth shard is shard 2 + const authShard = shards[1]!; + + // Recovery shard is shard 3, encrypted + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + authShard, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + expect(result).toHaveProperty("deviceShard"); + expect(result).toHaveProperty("authShard"); + expect(result).toHaveProperty("fullKey"); + }); + + it("returns the original mnemonic as fullKey", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + expect(result.fullKey).toBe(testMnemonic); + }); + + it("returns new device shard encrypted with new key", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + // Device shard should be decryptable with new device key + expect(typeof result.deviceShard).toBe("string"); + const decrypted = await decryptWithCipher({ + encryptedDataJSON: result.deviceShard, + key: newDeviceKey, + }); + expect(decrypted).toMatch(/^[0-9a-f]+$/); // hex-encoded shard + }); + + it("returns new auth shard as hex string", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + expect(typeof result.authShard).toBe("string"); + expect(result.authShard).toMatch(/^[0-9a-f]+$/); + }); + + it("new shards can reconstruct the original mnemonic", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + // Decrypt new device shard + const newShard1 = await decryptWithCipher({ + encryptedDataJSON: result.deviceShard, + key: newDeviceKey, + }); + const newShard2 = result.authShard; + + // Combine new shards + const share1 = hexToBytes(newShard1); + const share2 = hexToBytes(newShard2); + const reconstructed = await shamirCombine([share1, share2]); + const mnemonic = bytesToString(reconstructed); + + expect(mnemonic).toBe(testMnemonic); + }); + + it("device shard cannot be decrypted with wrong key", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + const wrongKey = await deriveKeyFromPassword("wrong-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + await expect( + decryptWithCipher({ + encryptedDataJSON: result.deviceShard, + key: wrongKey, + }), + ).rejects.toThrow(); + }); + + it("throws with wrong recovery key", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + const shards = await spiltKeyIntoShards(testMnemonic); + const correctRecoveryKey = await deriveKeyFromPassword("recovery-password"); + const wrongRecoveryKey = await deriveKeyFromPassword("wrong-recovery"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: correctRecoveryKey, + }); + + await expect( + clientRecovery( + shards[1]!, + encryptedRecoveryShard, + wrongRecoveryKey, + newDeviceKey, + ), + ).rejects.toThrow("Invalid recovery answer"); + consoleSpy.mockRestore(); + }); + + it("throws with corrupted recovery shard", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + // Corrupt the encrypted data + const parsed = JSON.parse(encryptedRecoveryShard); + parsed.ciphertext = "corrupted" + parsed.ciphertext; + + await expect( + clientRecovery( + shards[1]!, + JSON.stringify(parsed), + recoveryKey, + newDeviceKey, + ), + ).rejects.toThrow("Invalid recovery answer"); + consoleSpy.mockRestore(); + }); + + it("throws with invalid auth shard", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + await expect( + clientRecovery( + "invalid-hex!@#", + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ), + ).rejects.toThrow("Invalid recovery answer"); + consoleSpy.mockRestore(); + }); + + it("throws with invalid JSON in recovery shard", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + await expect( + clientRecovery(shards[1]!, "not-valid-json", recoveryKey, newDeviceKey), + ).rejects.toThrow("Invalid recovery answer"); + consoleSpy.mockRestore(); + }); +}); + +describe("clientRecovery with 24-word mnemonic", () => { + const mnemonic24 = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + + it("recovers 24-word mnemonic", async () => { + const shards = await spiltKeyIntoShards(mnemonic24); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + expect(result.fullKey).toBe(mnemonic24); + expect(result.fullKey.split(" ").length).toBe(24); + }); +}); + +describe("clientRecovery generates new shards", () => { + const testMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + it("generates different shards than original", async () => { + const originalShards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey = await deriveKeyFromPassword("new-device-password"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: originalShards[2]!, + key: recoveryKey, + }); + + const result = await clientRecovery( + originalShards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey, + ); + + // New auth shard should be different from original (due to re-splitting) + // Note: There's a tiny probability they could be the same, but it's negligible + expect(result.authShard).not.toBe(originalShards[1]); + }); + + it("recovery can be performed multiple times", async () => { + const shards = await spiltKeyIntoShards(testMnemonic); + const recoveryKey = await deriveKeyFromPassword("recovery-password"); + const newDeviceKey1 = await deriveKeyFromPassword("new-device-1"); + const newDeviceKey2 = await deriveKeyFromPassword("new-device-2"); + + const encryptedRecoveryShard = await encryptWithCipher({ + data: shards[2]!, + key: recoveryKey, + }); + + const result1 = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey1, + ); + + const result2 = await clientRecovery( + shards[1]!, + encryptedRecoveryShard, + recoveryKey, + newDeviceKey2, + ); + + // Both should recover the same mnemonic + expect(result1.fullKey).toBe(testMnemonic); + expect(result2.fullKey).toBe(testMnemonic); + + // But device shards should be encrypted with different keys + await expect( + decryptWithCipher({ + encryptedDataJSON: result1.deviceShard, + key: newDeviceKey2, + }), + ).rejects.toThrow(); + }); +}); diff --git a/src/functions/convertors.test.ts b/src/functions/convertors.test.ts new file mode 100644 index 0000000..e63ba40 --- /dev/null +++ b/src/functions/convertors.test.ts @@ -0,0 +1,146 @@ +import { + stringToBytes, + bytesToString, + bytesToHex, + hexToBytes, +} from "./convertors"; + +describe("stringToBytes", () => { + it("converts ASCII string to bytes", () => { + const bytes = stringToBytes("hello"); + expect(bytes).toBeInstanceOf(Uint8Array); + expect(Array.from(bytes)).toEqual([104, 101, 108, 108, 111]); + }); + + it("converts empty string to empty array", () => { + const bytes = stringToBytes(""); + expect(bytes.length).toBe(0); + }); + + it("converts unicode string to bytes", () => { + const bytes = stringToBytes("ๆ—ฅๆœฌ"); + expect(bytes.length).toBeGreaterThan(2); // UTF-8 multibyte + }); + + it("handles special characters", () => { + const bytes = stringToBytes("!@#$%"); + expect(bytes.length).toBe(5); + }); +}); + +describe("bytesToString", () => { + it("converts bytes back to ASCII string", () => { + const bytes = new Uint8Array([104, 101, 108, 108, 111]); + expect(bytesToString(bytes)).toBe("hello"); + }); + + it("converts empty array to empty string", () => { + const bytes = new Uint8Array([]); + expect(bytesToString(bytes)).toBe(""); + }); + + it("roundtrip: string -> bytes -> string", () => { + const original = "Hello, World! 123"; + const bytes = stringToBytes(original); + const result = bytesToString(bytes); + expect(result).toBe(original); + }); + + it("handles unicode roundtrip", () => { + const original = "ๆ—ฅๆœฌ่ชžใƒ†ใ‚นใƒˆ"; + const bytes = stringToBytes(original); + const result = bytesToString(bytes); + expect(result).toBe(original); + }); +}); + +describe("bytesToHex", () => { + it("converts bytes to hex string", () => { + const bytes = new Uint8Array([0, 15, 16, 255]); + expect(bytesToHex(bytes)).toBe("000f10ff"); + }); + + it("converts empty array to empty string", () => { + const bytes = new Uint8Array([]); + expect(bytesToHex(bytes)).toBe(""); + }); + + it("pads single digit hex values", () => { + const bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(bytesToHex(bytes)).toBe("00010203040506070809"); + }); + + it("handles large values correctly", () => { + const bytes = new Uint8Array([255, 254, 253]); + expect(bytesToHex(bytes)).toBe("fffefd"); + }); + + it("produces lowercase hex", () => { + const bytes = new Uint8Array([171, 205, 239]); + expect(bytesToHex(bytes)).toBe("abcdef"); + expect(bytesToHex(bytes)).not.toMatch(/[A-F]/); + }); +}); + +describe("hexToBytes", () => { + it("converts hex string to bytes", () => { + const bytes = hexToBytes("000f10ff"); + expect(Array.from(bytes)).toEqual([0, 15, 16, 255]); + }); + + it("converts empty string to empty array", () => { + const bytes = hexToBytes(""); + expect(bytes.length).toBe(0); + }); + + it("handles lowercase hex", () => { + const bytes = hexToBytes("abcdef"); + expect(Array.from(bytes)).toEqual([171, 205, 239]); + }); + + it("handles uppercase hex", () => { + const bytes = hexToBytes("ABCDEF"); + expect(Array.from(bytes)).toEqual([171, 205, 239]); + }); + + it("handles mixed case hex", () => { + const bytes = hexToBytes("AbCdEf"); + expect(Array.from(bytes)).toEqual([171, 205, 239]); + }); + + it("roundtrip: bytes -> hex -> bytes", () => { + const original = new Uint8Array([0, 127, 128, 255, 1, 2, 3]); + const hex = bytesToHex(original); + const result = hexToBytes(hex); + expect(Array.from(result)).toEqual(Array.from(original)); + }); +}); + +describe("full roundtrip: string -> bytes -> hex -> bytes -> string", () => { + it("preserves ASCII string through full conversion cycle", () => { + const original = "hello world"; + const bytes1 = stringToBytes(original); + const hex = bytesToHex(bytes1); + const bytes2 = hexToBytes(hex); + const result = bytesToString(bytes2); + expect(result).toBe(original); + }); + + it("preserves unicode string through full conversion cycle", () => { + const original = "ใ“ใ‚“ใซใกใฏ"; + const bytes1 = stringToBytes(original); + const hex = bytesToHex(bytes1); + const bytes2 = hexToBytes(hex); + const result = bytesToString(bytes2); + expect(result).toBe(original); + }); + + it("handles large data", () => { + const original = "x".repeat(10000); + const bytes1 = stringToBytes(original); + const hex = bytesToHex(bytes1); + const bytes2 = hexToBytes(hex); + const result = bytesToString(bytes2); + expect(result).toBe(original); + }); +}); diff --git a/src/functions/crypto/encryption.test.ts b/src/functions/crypto/encryption.test.ts index c0646ef..7e2f379 100644 --- a/src/functions/crypto/encryption.test.ts +++ b/src/functions/crypto/encryption.test.ts @@ -10,7 +10,10 @@ import { const data = "solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution solution"; -async function deriveKeyFromPassword(password: string): Promise { +async function deriveKeyFromPassword( + password: string, + usages: KeyUsage[] = ["encrypt", "decrypt"], +): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( "raw", @@ -29,7 +32,7 @@ async function deriveKeyFromPassword(password: string): Promise { keyMaterial, { name: "AES-GCM", length: 256 }, false, - ["encrypt", "decrypt"], + usages, ); } @@ -42,7 +45,6 @@ describe("with cipher", () => { data, key, }); - console.log("encryptedDataJSON", encryptedDataJSON); const decrypted = await decryptWithCipher({ encryptedDataJSON: encryptedDataJSON, @@ -51,6 +53,93 @@ describe("with cipher", () => { expect(data).toBe(decrypted); }); + + it("encrypts empty string", async () => { + const key = await deriveKeyFromPassword(keyString); + const encryptedDataJSON = await encryptWithCipher({ + data: "", + key, + }); + const decrypted = await decryptWithCipher({ + encryptedDataJSON, + key, + }); + expect(decrypted).toBe(""); + }); + + it("encrypts unicode characters", async () => { + const key = await deriveKeyFromPassword(keyString); + const unicodeData = "Hello ๆ—ฅๆœฌ่ชž ๐ŸŽ‰"; + const encryptedDataJSON = await encryptWithCipher({ + data: unicodeData, + key, + }); + const decrypted = await decryptWithCipher({ + encryptedDataJSON, + key, + }); + expect(decrypted).toBe(unicodeData); + }); + + it("produces different ciphertext for same input (due to random IV)", async () => { + const key = await deriveKeyFromPassword(keyString); + const encrypted1 = await encryptWithCipher({ data, key }); + const encrypted2 = await encryptWithCipher({ data, key }); + expect(encrypted1).not.toBe(encrypted2); + }); + + it("fails to decrypt with wrong key", async () => { + const key1 = await deriveKeyFromPassword("password1"); + const key2 = await deriveKeyFromPassword("password2"); + const encryptedDataJSON = await encryptWithCipher({ + data, + key: key1, + }); + await expect( + decryptWithCipher({ encryptedDataJSON, key: key2 }), + ).rejects.toThrow(); + }); + + it("fails to decrypt corrupted ciphertext", async () => { + const key = await deriveKeyFromPassword(keyString); + const encryptedDataJSON = await encryptWithCipher({ data, key }); + const parsed = JSON.parse(encryptedDataJSON); + parsed.ciphertext = "corrupted" + parsed.ciphertext; + await expect( + decryptWithCipher({ encryptedDataJSON: JSON.stringify(parsed), key }), + ).rejects.toThrow(); + }); + + it("fails to decrypt with corrupted IV", async () => { + const key = await deriveKeyFromPassword(keyString); + const encryptedDataJSON = await encryptWithCipher({ data, key }); + const parsed = JSON.parse(encryptedDataJSON); + parsed.iv = "AAAAAAAAAAAAAAAAAAAAAA=="; // Different IV + await expect( + decryptWithCipher({ encryptedDataJSON: JSON.stringify(parsed), key }), + ).rejects.toThrow(); + }); + + it("fails to decrypt invalid JSON", async () => { + const key = await deriveKeyFromPassword(keyString); + await expect( + decryptWithCipher({ encryptedDataJSON: "not valid json", key }), + ).rejects.toThrow(); + }); + + it("handles large data", async () => { + const key = await deriveKeyFromPassword(keyString); + const largeData = "x".repeat(100000); + const encryptedDataJSON = await encryptWithCipher({ + data: largeData, + key, + }); + const decrypted = await decryptWithCipher({ + encryptedDataJSON, + key, + }); + expect(decrypted).toBe(largeData); + }); }); describe("with keypair", () => { @@ -66,4 +155,145 @@ describe("with keypair", () => { expect(data).toBe(decrypted); }); + + it("generates unique keypairs", async () => { + const keyPair1 = await generateKeyPair(); + const keyPair2 = await generateKeyPair(); + expect(keyPair1.publicKey).not.toBe(keyPair2.publicKey); + expect(keyPair1.privateKey).not.toBe(keyPair2.privateKey); + }); + + it("encrypts empty string", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + const encryptedDataJSON = await encryptWithPublicKey({ + publicKey, + data: "", + }); + const decrypted = await decryptWithPrivateKey({ + privateKey, + encryptedDataJSON, + }); + expect(decrypted).toBe(""); + }); + + it("encrypts unicode characters", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + const unicodeData = "Hello ไธ–็•Œ ๐ŸŒ"; + const encryptedDataJSON = await encryptWithPublicKey({ + publicKey, + data: unicodeData, + }); + const decrypted = await decryptWithPrivateKey({ + privateKey, + encryptedDataJSON, + }); + expect(decrypted).toBe(unicodeData); + }); + + it("produces different ciphertext for same input (ephemeral key)", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + const encrypted1 = await encryptWithPublicKey({ publicKey, data }); + const encrypted2 = await encryptWithPublicKey({ publicKey, data }); + expect(encrypted1).not.toBe(encrypted2); + // Both should decrypt to same value + const decrypted1 = await decryptWithPrivateKey({ + privateKey, + encryptedDataJSON: encrypted1, + }); + const decrypted2 = await decryptWithPrivateKey({ + privateKey, + encryptedDataJSON: encrypted2, + }); + expect(decrypted1).toBe(data); + expect(decrypted2).toBe(data); + }); + + it("fails to decrypt with wrong private key", async () => { + const keyPair1 = await generateKeyPair(); + const keyPair2 = await generateKeyPair(); + const encryptedDataJSON = await encryptWithPublicKey({ + publicKey: keyPair1.publicKey, + data, + }); + await expect( + decryptWithPrivateKey({ + privateKey: keyPair2.privateKey, + encryptedDataJSON, + }), + ).rejects.toThrow(); + }); + + it("fails to decrypt with invalid private key format", async () => { + const { publicKey } = await generateKeyPair(); + const encryptedDataJSON = await encryptWithPublicKey({ publicKey, data }); + await expect( + decryptWithPrivateKey({ + privateKey: "not-a-valid-key", + encryptedDataJSON, + }), + ).rejects.toThrow(); + }); + + it("fails to encrypt with invalid public key format", async () => { + await expect( + encryptWithPublicKey({ + publicKey: "not-a-valid-key", + data, + }), + ).rejects.toThrow(); + }); + + it("fails to decrypt corrupted ciphertext", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + const encryptedDataJSON = await encryptWithPublicKey({ publicKey, data }); + const parsed = JSON.parse(encryptedDataJSON); + parsed.ciphertext = "corrupted" + parsed.ciphertext; + await expect( + decryptWithPrivateKey({ + privateKey, + encryptedDataJSON: JSON.stringify(parsed), + }), + ).rejects.toThrow(); + }); + + it("fails to decrypt invalid JSON", async () => { + const { privateKey } = await generateKeyPair(); + await expect( + decryptWithPrivateKey({ + privateKey, + encryptedDataJSON: "not valid json", + }), + ).rejects.toThrow(); + }); + + it("handles large data", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + const largeData = "y".repeat(50000); + const encryptedDataJSON = await encryptWithPublicKey({ + publicKey, + data: largeData, + }); + const decrypted = await decryptWithPrivateKey({ + privateKey, + encryptedDataJSON, + }); + expect(decrypted).toBe(largeData); + }); +}); + +describe("generateKeyPair", () => { + it("returns base64 encoded keys", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + // Base64 strings should not throw when decoded + expect(() => Buffer.from(publicKey, "base64")).not.toThrow(); + expect(() => Buffer.from(privateKey, "base64")).not.toThrow(); + }); + + it("generates keys of expected format (SPKI for public, PKCS8 for private)", async () => { + const { publicKey, privateKey } = await generateKeyPair(); + // SPKI encoded P-256 public keys are typically 91 bytes -> ~122 base64 chars + // PKCS8 encoded P-256 private keys are typically 138 bytes -> ~184 base64 chars + expect(publicKey.length).toBeGreaterThan(100); + expect(privateKey.length).toBeGreaterThan(150); + }); }); diff --git a/src/functions/crypto/hash.test.ts b/src/functions/crypto/hash.test.ts new file mode 100644 index 0000000..c11c368 --- /dev/null +++ b/src/functions/crypto/hash.test.ts @@ -0,0 +1,102 @@ +import { generateHash, hashData } from "./hash"; + +describe("generateHash", () => { + it("generates a hex string of default size (64 bytes = 128 hex chars)", async () => { + const hash = await generateHash({}); + expect(typeof hash).toBe("string"); + expect(hash).toMatch(/^[0-9a-f]+$/); + expect(hash.length).toBe(128); // 64 bytes = 128 hex characters + }); + + it("generates a hex string of custom size", async () => { + const hash = await generateHash({ size: 32 }); + expect(hash.length).toBe(64); // 32 bytes = 64 hex characters + }); + + it("generates a hex string of small size", async () => { + const hash = await generateHash({ size: 8 }); + expect(hash.length).toBe(16); // 8 bytes = 16 hex characters + }); + + it("generates unique hashes on each call", async () => { + const hash1 = await generateHash({}); + const hash2 = await generateHash({}); + expect(hash1).not.toBe(hash2); + }); +}); + +describe("hashData", () => { + it("hashes data with sha256 by default", async () => { + const hash = await hashData({ data: "hello world" }); + expect(typeof hash).toBe("string"); + expect(hash).toMatch(/^[0-9a-f]+$/); + expect(hash.length).toBe(64); // SHA-256 = 32 bytes = 64 hex characters + }); + + it("produces consistent hash for same input", async () => { + const hash1 = await hashData({ data: "test data" }); + const hash2 = await hashData({ data: "test data" }); + expect(hash1).toBe(hash2); + }); + + it("produces different hash for different input", async () => { + const hash1 = await hashData({ data: "data1" }); + const hash2 = await hashData({ data: "data2" }); + expect(hash1).not.toBe(hash2); + }); + + it("hashes data with a private key (HMAC)", async () => { + const hash = await hashData({ + data: "hello world", + privateKey: "secret-key", + }); + expect(typeof hash).toBe("string"); + expect(hash.length).toBe(64); + }); + + it("produces different hash with different private keys", async () => { + const hash1 = await hashData({ data: "test", privateKey: "key1" }); + const hash2 = await hashData({ data: "test", privateKey: "key2" }); + expect(hash1).not.toBe(hash2); + }); + + it("produces consistent HMAC hash for same input and key", async () => { + const hash1 = await hashData({ data: "test", privateKey: "secret" }); + const hash2 = await hashData({ data: "test", privateKey: "secret" }); + expect(hash1).toBe(hash2); + }); + + it("hashes data with sha512 algorithm", async () => { + const hash = await hashData({ + data: "hello world", + algorithm: "sha512", + }); + expect(hash.length).toBe(128); // SHA-512 = 64 bytes = 128 hex characters + }); + + it("hashes data with sha1 algorithm", async () => { + const hash = await hashData({ + data: "hello world", + algorithm: "sha1", + }); + expect(hash.length).toBe(40); // SHA-1 = 20 bytes = 40 hex characters + }); + + it("handles empty string input", async () => { + const hash = await hashData({ data: "" }); + expect(typeof hash).toBe("string"); + expect(hash.length).toBe(64); + }); + + it("handles special characters", async () => { + const hash = await hashData({ data: "Hello! @#$%^&*() ๆ—ฅๆœฌ่ชž" }); + expect(typeof hash).toBe("string"); + expect(hash.length).toBe(64); + }); + + it("handles numeric data converted to string", async () => { + const hash = await hashData({ data: "12345" }); + expect(typeof hash).toBe("string"); + expect(hash.length).toBe(64); + }); +}); diff --git a/src/functions/key-shard/combine-shards-build-wallet.test.ts b/src/functions/key-shard/combine-shards-build-wallet.test.ts new file mode 100644 index 0000000..d417a71 --- /dev/null +++ b/src/functions/key-shard/combine-shards-build-wallet.test.ts @@ -0,0 +1,267 @@ +import { spiltKeyIntoShards } from "./spilt-key-into-shards"; +import { hexToBytes, bytesToString } from "../convertors"; +import { shamirCombine } from "./shamir-secret-sharing"; + +// Mock external wallet SDKs +jest.mock("@meshsdk/wallet", () => ({ + MeshWallet: jest.fn().mockImplementation(() => ({ + init: jest.fn().mockResolvedValue(undefined), + getUsedAddresses: jest.fn().mockResolvedValue(["addr_test1..."]), + })), +})); + +jest.mock("@meshsdk/bitcoin", () => ({ + EmbeddedWallet: jest.fn().mockImplementation(() => ({ + getAddress: jest.fn().mockResolvedValue("bc1q..."), + })), +})); + +jest.mock("@buildonspark/spark-sdk", () => ({ + SparkWallet: { + initialize: jest.fn().mockResolvedValue({ + wallet: { + getAddress: jest.fn().mockResolvedValue("spark1..."), + }, + }), + }, +})); + +// Import after mocks are set up +import { combineShardsBuildWallet } from "./combine-shards-build-wallet"; +import { MeshWallet } from "@meshsdk/wallet"; +import { EmbeddedWallet } from "@meshsdk/bitcoin"; +import { SparkWallet } from "@buildonspark/spark-sdk"; + +describe("combineShardsBuildWallet", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("reconstructs mnemonic from 2 shards and creates wallets", async () => { + const originalMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(originalMnemonic); + + const result = await combineShardsBuildWallet(0, shards[0]!, shards[1]!); + + expect(result.key).toBe(originalMnemonic); + expect(result.bitcoinWallet).toBeDefined(); + expect(result.cardanoWallet).toBeDefined(); + expect(result.sparkWallet).toBeDefined(); + }); + + it("works with any 2-of-3 shard combination", async () => { + const mnemonic = + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"; + const shards = await spiltKeyIntoShards(mnemonic); + + // Test all combinations + const result1 = await combineShardsBuildWallet(0, shards[0]!, shards[1]!); + const result2 = await combineShardsBuildWallet(0, shards[0]!, shards[2]!); + const result3 = await combineShardsBuildWallet(0, shards[1]!, shards[2]!); + + expect(result1.key).toBe(mnemonic); + expect(result2.key).toBe(mnemonic); + expect(result3.key).toBe(mnemonic); + }); + + it("initializes EmbeddedWallet with correct network for testnet", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + await combineShardsBuildWallet(0, shards[0]!, shards[1]!); + + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ + network: "Testnet", + key: expect.objectContaining({ + type: "mnemonic", + words: mnemonic.split(" "), + }), + }), + ); + }); + + it("initializes EmbeddedWallet with correct network for mainnet", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + await combineShardsBuildWallet(1, shards[0]!, shards[1]!); + + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ + network: "Mainnet", + key: expect.objectContaining({ + type: "mnemonic", + words: mnemonic.split(" "), + }), + }), + ); + }); + + it("initializes MeshWallet with correct networkId for testnet", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + await combineShardsBuildWallet(0, shards[0]!, shards[1]!); + + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ + networkId: 0, + key: expect.objectContaining({ + type: "mnemonic", + words: mnemonic.split(" "), + }), + }), + ); + }); + + it("initializes MeshWallet with correct networkId for mainnet", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + await combineShardsBuildWallet(1, shards[0]!, shards[1]!); + + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ + networkId: 1, + key: expect.objectContaining({ + type: "mnemonic", + words: mnemonic.split(" "), + }), + }), + ); + }); + + it("initializes SparkWallet with REGTEST for testnet", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + await combineShardsBuildWallet(0, shards[0]!, shards[1]!); + + expect(SparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + mnemonicOrSeed: mnemonic, + options: expect.objectContaining({ + network: "REGTEST", + }), + }), + ); + }); + + it("initializes SparkWallet with MAINNET for mainnet", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + await combineShardsBuildWallet(1, shards[0]!, shards[1]!); + + expect(SparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + mnemonicOrSeed: mnemonic, + options: expect.objectContaining({ + network: "MAINNET", + }), + }), + ); + }); + + it("calls cardanoWallet.init()", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + const result = await combineShardsBuildWallet(0, shards[0]!, shards[1]!); + + expect(result.cardanoWallet.init).toHaveBeenCalled(); + }); + + it("passes bitcoinProvider to EmbeddedWallet when provided", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + const mockProvider = { getUtxos: jest.fn() }; + + await combineShardsBuildWallet( + 0, + shards[0]!, + shards[1]!, + mockProvider as any, + ); + + expect(EmbeddedWallet).toHaveBeenCalledWith( + expect.objectContaining({ + provider: mockProvider, + }), + ); + }); + + it("handles 24-word mnemonic", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + const shards = await spiltKeyIntoShards(mnemonic); + + const result = await combineShardsBuildWallet(0, shards[0]!, shards[2]!); + + expect(result.key).toBe(mnemonic); + expect(result.key.split(" ").length).toBe(24); + }); +}); + +describe("combineShardsBuildWallet error cases", () => { + it("fails with invalid hex shards", async () => { + await expect( + combineShardsBuildWallet(0, "invalid-hex!", "also-invalid!"), + ).rejects.toThrow(); + }); + + it("fails with mismatched shard lengths", async () => { + const mnemonic1 = "short"; + const mnemonic2 = "much longer mnemonic phrase here"; + const shards1 = await spiltKeyIntoShards(mnemonic1); + const shards2 = await spiltKeyIntoShards(mnemonic2); + + await expect( + combineShardsBuildWallet(0, shards1[0]!, shards2[1]!), + ).rejects.toThrow("all shares must have the same byte length"); + }); + + it("fails with shards from different secrets of same length", async () => { + // Both mnemonics have same byte length but different content + const mnemonic1 = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const mnemonic2 = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon actor"; + const shards1 = await spiltKeyIntoShards(mnemonic1); + const shards2 = await spiltKeyIntoShards(mnemonic2); + + // This won't throw but will produce garbage - testing the operation completes + // (In real use, the resulting wallet would be unusable) + const result = await combineShardsBuildWallet(0, shards1[0]!, shards2[1]!); + + // Result won't match either original mnemonic (demonstrates corruption) + expect(result.key).not.toBe(mnemonic1); + expect(result.key).not.toBe(mnemonic2); + }); +}); + +describe("shard reconstruction without wallet creation", () => { + // These tests verify the core shard logic independently of wallet mocks + it("shamirCombine correctly reconstructs from hex shards", async () => { + const originalMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(originalMnemonic); + + const share1 = hexToBytes(shards[0]!); + const share2 = hexToBytes(shards[1]!); + const reconstructed = await shamirCombine([share1, share2]); + const result = bytesToString(reconstructed); + + expect(result).toBe(originalMnemonic); + }); +}); diff --git a/src/functions/key-shard/shamir-secret-sharing.test.ts b/src/functions/key-shard/shamir-secret-sharing.test.ts new file mode 100644 index 0000000..e243145 --- /dev/null +++ b/src/functions/key-shard/shamir-secret-sharing.test.ts @@ -0,0 +1,226 @@ +import { shamirSplit, shamirCombine } from "./shamir-secret-sharing"; + +describe("shamirSplit", () => { + it("splits a secret into the requested number of shares", async () => { + const secret = new Uint8Array([1, 2, 3, 4, 5]); + const shares = await shamirSplit(secret, 3, 2); + expect(shares.length).toBe(3); + }); + + it("each share has length = secret.length + 1", async () => { + const secret = new Uint8Array([1, 2, 3, 4, 5]); + const shares = await shamirSplit(secret, 3, 2); + for (const share of shares) { + expect(share.length).toBe(6); // 5 + 1 for x-coordinate + } + }); + + it("produces different shares for same secret (randomized)", async () => { + const secret = new Uint8Array([1, 2, 3, 4, 5]); + const shares1 = await shamirSplit(secret, 3, 2); + const shares2 = await shamirSplit(secret, 3, 2); + // At least one share should be different (extremely unlikely to be same) + const allSame = shares1.every((s1, i) => { + const s2 = shares2[i]!; + return s1.every((b, j) => b === s2[j]); + }); + expect(allSame).toBe(false); + }); + + it("works with different threshold values", async () => { + const secret = new Uint8Array([10, 20, 30]); + const shares = await shamirSplit(secret, 5, 3); + expect(shares.length).toBe(5); + }); + + it("throws if secret is empty", async () => { + const secret = new Uint8Array([]); + await expect(shamirSplit(secret, 3, 2)).rejects.toThrow( + "secret cannot be empty", + ); + }); + + it("throws if shares < 2", async () => { + const secret = new Uint8Array([1, 2, 3]); + await expect(shamirSplit(secret, 1, 1)).rejects.toThrow( + "shares must be at least 2 and at most 255", + ); + }); + + it("throws if shares > 255", async () => { + const secret = new Uint8Array([1, 2, 3]); + await expect(shamirSplit(secret, 256, 2)).rejects.toThrow( + "shares must be at least 2 and at most 255", + ); + }); + + it("throws if threshold < 2", async () => { + const secret = new Uint8Array([1, 2, 3]); + await expect(shamirSplit(secret, 3, 1)).rejects.toThrow( + "threshold must be at least 2 and at most 255", + ); + }); + + it("throws if threshold > 255", async () => { + const secret = new Uint8Array([1, 2, 3]); + await expect(shamirSplit(secret, 3, 256)).rejects.toThrow( + "threshold must be at least 2 and at most 255", + ); + }); + + it("throws if shares < threshold", async () => { + const secret = new Uint8Array([1, 2, 3]); + await expect(shamirSplit(secret, 2, 3)).rejects.toThrow( + "shares cannot be less than threshold", + ); + }); + + it("throws if secret is not Uint8Array", async () => { + await expect( + shamirSplit([1, 2, 3] as unknown as Uint8Array, 3, 2), + ).rejects.toThrow("secret must be a Uint8Array"); + }); +}); + +describe("shamirCombine", () => { + it("reconstructs secret from threshold number of shares", async () => { + const secret = new Uint8Array([1, 2, 3, 4, 5]); + const shares = await shamirSplit(secret, 3, 2); + const reconstructed = await shamirCombine([shares[0]!, shares[1]!]); + expect(Array.from(reconstructed)).toEqual(Array.from(secret)); + }); + + it("reconstructs secret from any 2 shares in 2-of-3 scheme", async () => { + const secret = new Uint8Array([10, 20, 30, 40, 50]); + const shares = await shamirSplit(secret, 3, 2); + + // Try all combinations of 2 shares + const combo1 = await shamirCombine([shares[0]!, shares[1]!]); + const combo2 = await shamirCombine([shares[0]!, shares[2]!]); + const combo3 = await shamirCombine([shares[1]!, shares[2]!]); + + expect(Array.from(combo1)).toEqual(Array.from(secret)); + expect(Array.from(combo2)).toEqual(Array.from(secret)); + expect(Array.from(combo3)).toEqual(Array.from(secret)); + }); + + it("reconstructs secret from all 3 shares", async () => { + const secret = new Uint8Array([1, 2, 3]); + const shares = await shamirSplit(secret, 3, 2); + const reconstructed = await shamirCombine(shares); + expect(Array.from(reconstructed)).toEqual(Array.from(secret)); + }); + + it("reconstructs secret in 3-of-5 scheme", async () => { + const secret = new Uint8Array([11, 22, 33, 44]); + const shares = await shamirSplit(secret, 5, 3); + + // Need at least 3 shares + const reconstructed = await shamirCombine([ + shares[0]!, + shares[2]!, + shares[4]!, + ]); + expect(Array.from(reconstructed)).toEqual(Array.from(secret)); + }); + + it("handles single-byte secret", async () => { + const secret = new Uint8Array([42]); + const shares = await shamirSplit(secret, 3, 2); + const reconstructed = await shamirCombine([shares[0]!, shares[1]!]); + expect(Array.from(reconstructed)).toEqual([42]); + }); + + it("handles large secret", async () => { + const secret = new Uint8Array(256); + for (let i = 0; i < 256; i++) { + secret[i] = i; + } + const shares = await shamirSplit(secret, 3, 2); + const reconstructed = await shamirCombine([shares[0]!, shares[2]!]); + expect(Array.from(reconstructed)).toEqual(Array.from(secret)); + }); + + it("throws if shares array is empty", async () => { + await expect(shamirCombine([])).rejects.toThrow( + "shares must have at least 2 and at most 255 elements", + ); + }); + + it("throws if only 1 share provided", async () => { + const secret = new Uint8Array([1, 2, 3]); + const shares = await shamirSplit(secret, 3, 2); + await expect(shamirCombine([shares[0]!])).rejects.toThrow( + "shares must have at least 2 and at most 255 elements", + ); + }); + + it("throws if shares have different lengths", async () => { + const share1 = new Uint8Array([1, 2, 3, 10]); + const share2 = new Uint8Array([4, 5, 20]); + await expect(shamirCombine([share1, share2])).rejects.toThrow( + "all shares must have the same byte length", + ); + }); + + it("throws if share is too short (< 2 bytes)", async () => { + const share1 = new Uint8Array([1]); + const share2 = new Uint8Array([2]); + await expect(shamirCombine([share1, share2])).rejects.toThrow( + "each share must be at least 2 bytes", + ); + }); + + it("throws if duplicate x-coordinates", async () => { + // Create two shares with same x-coordinate (last byte) + const share1 = new Uint8Array([1, 2, 3, 10]); + const share2 = new Uint8Array([4, 5, 6, 10]); // Same x-coordinate + await expect(shamirCombine([share1, share2])).rejects.toThrow( + "shares must contain unique values but a duplicate was found", + ); + }); + + it("throws if shares is not an Array", async () => { + await expect( + shamirCombine("not an array" as unknown as Uint8Array[]), + ).rejects.toThrow("shares must be an Array"); + }); + + it("throws if share is not Uint8Array", async () => { + await expect( + shamirCombine([[1, 2, 3, 10], [4, 5, 6, 20]] as unknown as Uint8Array[]), + ).rejects.toThrow("each share must be a Uint8Array"); + }); +}); + +describe("shamirSplit and shamirCombine roundtrip", () => { + it("preserves secret through split and combine", async () => { + const originalSecret = new TextEncoder().encode("hello world secret"); + const shares = await shamirSplit(originalSecret, 5, 3); + const reconstructed = await shamirCombine([ + shares[1]!, + shares[3]!, + shares[4]!, + ]); + const decoded = new TextDecoder().decode(reconstructed); + expect(decoded).toBe("hello world secret"); + }); + + it("preserves mnemonic through split and combine", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const secret = new TextEncoder().encode(mnemonic); + const shares = await shamirSplit(secret, 3, 2); + const reconstructed = await shamirCombine([shares[0]!, shares[2]!]); + const decoded = new TextDecoder().decode(reconstructed); + expect(decoded).toBe(mnemonic); + }); + + it("handles unicode content", async () => { + const unicodeSecret = new TextEncoder().encode("Hello ไธ–็•Œ ๐ŸŽ‰ ะฟั€ะธะฒะตั‚"); + const shares = await shamirSplit(unicodeSecret, 3, 2); + const reconstructed = await shamirCombine([shares[1]!, shares[2]!]); + const decoded = new TextDecoder().decode(reconstructed); + expect(decoded).toBe("Hello ไธ–็•Œ ๐ŸŽ‰ ะฟั€ะธะฒะตั‚"); + }); +}); diff --git a/src/functions/key-shard/spilt-key-into-shards.test.ts b/src/functions/key-shard/spilt-key-into-shards.test.ts new file mode 100644 index 0000000..c24e5b5 --- /dev/null +++ b/src/functions/key-shard/spilt-key-into-shards.test.ts @@ -0,0 +1,165 @@ +import { spiltKeyIntoShards } from "./spilt-key-into-shards"; +import { hexToBytes, bytesToString } from "../convertors"; +import { shamirCombine } from "./shamir-secret-sharing"; + +describe("spiltKeyIntoShards", () => { + it("splits a mnemonic into 3 hex-encoded shards", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + expect(shards.length).toBe(3); + expect(typeof shards[0]).toBe("string"); + expect(typeof shards[1]).toBe("string"); + expect(typeof shards[2]).toBe("string"); + }); + + it("produces hex-encoded shards", async () => { + const key = "test secret key"; + const shards = await spiltKeyIntoShards(key); + + // Each shard should be valid hex (only 0-9, a-f characters) + for (const shard of shards) { + expect(shard).toMatch(/^[0-9a-f]+$/); + } + }); + + it("produces unique shards", async () => { + const key = "some secret"; + const shards = await spiltKeyIntoShards(key); + + expect(shards[0]).not.toBe(shards[1]); + expect(shards[0]).not.toBe(shards[2]); + expect(shards[1]).not.toBe(shards[2]); + }); + + it("produces shards of consistent length", async () => { + const key = "test key 12345"; + const shards = await spiltKeyIntoShards(key); + + // All shards should have the same length (key bytes + 1 for x-coordinate, then hex-encoded) + expect(shards[0]!.length).toBe(shards[1]!.length); + expect(shards[1]!.length).toBe(shards[2]!.length); + }); + + it("shard length is (key_length + 1) * 2 due to hex encoding", async () => { + const key = "hello"; // 5 characters = 5 bytes + const shards = await spiltKeyIntoShards(key); + + // Each shard: 5 bytes + 1 x-coordinate byte = 6 bytes = 12 hex chars + expect(shards[0]!.length).toBe(12); + }); + + it("handles empty string", async () => { + // Note: shamirSplit throws on empty secret, but spiltKeyIntoShards converts string to bytes first + // An empty string becomes an empty Uint8Array, which should throw + await expect(spiltKeyIntoShards("")).rejects.toThrow("secret cannot be empty"); + }); + + it("handles unicode strings", async () => { + const unicodeKey = "Hello ไธ–็•Œ ๐ŸŽ‰"; + const shards = await spiltKeyIntoShards(unicodeKey); + + expect(shards.length).toBe(3); + // Unicode characters take multiple bytes in UTF-8 + expect(shards[0]!.length).toBeGreaterThan(20); + }); + + it("handles long mnemonics", async () => { + const longMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + const shards = await spiltKeyIntoShards(longMnemonic); + + expect(shards.length).toBe(3); + }); + + it("produces different shards on each call (due to randomization)", async () => { + const key = "same key"; + const shards1 = await spiltKeyIntoShards(key); + const shards2 = await spiltKeyIntoShards(key); + + // At least one shard should differ between calls + const allSame = + shards1[0] === shards2[0] && + shards1[1] === shards2[1] && + shards1[2] === shards2[2]; + expect(allSame).toBe(false); + }); +}); + +describe("spiltKeyIntoShards 2-of-3 reconstruction", () => { + it("can reconstruct original key from shards[0] and shards[1]", async () => { + const originalKey = "my secret mnemonic phrase"; + const shards = await spiltKeyIntoShards(originalKey); + + const share1 = hexToBytes(shards[0]!); + const share2 = hexToBytes(shards[1]!); + const reconstructed = await shamirCombine([share1, share2]); + const result = bytesToString(reconstructed); + + expect(result).toBe(originalKey); + }); + + it("can reconstruct original key from shards[0] and shards[2]", async () => { + const originalKey = "my secret mnemonic phrase"; + const shards = await spiltKeyIntoShards(originalKey); + + const share1 = hexToBytes(shards[0]!); + const share3 = hexToBytes(shards[2]!); + const reconstructed = await shamirCombine([share1, share3]); + const result = bytesToString(reconstructed); + + expect(result).toBe(originalKey); + }); + + it("can reconstruct original key from shards[1] and shards[2]", async () => { + const originalKey = "my secret mnemonic phrase"; + const shards = await spiltKeyIntoShards(originalKey); + + const share2 = hexToBytes(shards[1]!); + const share3 = hexToBytes(shards[2]!); + const reconstructed = await shamirCombine([share2, share3]); + const result = bytesToString(reconstructed); + + expect(result).toBe(originalKey); + }); + + it("can reconstruct from all 3 shards", async () => { + const originalKey = "test key"; + const shards = await spiltKeyIntoShards(originalKey); + + const share1 = hexToBytes(shards[0]!); + const share2 = hexToBytes(shards[1]!); + const share3 = hexToBytes(shards[2]!); + const reconstructed = await shamirCombine([share1, share2, share3]); + const result = bytesToString(reconstructed); + + expect(result).toBe(originalKey); + }); + + it("preserves 24-word mnemonic through split and combine", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + const shards = await spiltKeyIntoShards(mnemonic); + + const share1 = hexToBytes(shards[0]!); + const share2 = hexToBytes(shards[2]!); + const reconstructed = await shamirCombine([share1, share2]); + const result = bytesToString(reconstructed); + + expect(result).toBe(mnemonic); + }); + + it("preserves 12-word mnemonic through split and combine", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const shards = await spiltKeyIntoShards(mnemonic); + + const share1 = hexToBytes(shards[1]!); + const share2 = hexToBytes(shards[2]!); + const reconstructed = await shamirCombine([share1, share2]); + const result = bytesToString(reconstructed); + + expect(result).toBe(mnemonic); + }); +}); diff --git a/src/sdk/wallet-developer-controlled/index.test.ts b/src/sdk/wallet-developer-controlled/index.test.ts new file mode 100644 index 0000000..51ec344 --- /dev/null +++ b/src/sdk/wallet-developer-controlled/index.test.ts @@ -0,0 +1,633 @@ +// Mock uuid before imports +jest.mock("uuid", () => ({ + v4: jest.fn().mockReturnValue("mock-wallet-id-uuid"), +})); + +// Mock external wallet SDKs +jest.mock("@meshsdk/wallet", () => ({ + MeshWallet: Object.assign( + jest.fn().mockImplementation(() => ({ + init: jest.fn().mockResolvedValue(undefined), + getAddresses: jest.fn().mockReturnValue({ + baseAddressBech32: + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + }), + })), + { + brew: jest.fn().mockReturnValue([ + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "about", + ]), + }, + ), +})); + +jest.mock("@meshsdk/core-cst", () => ({ + deserializeBech32Address: jest.fn().mockReturnValue({ + pubKeyHash: "mock-cardano-pub-key-hash", + stakeCredentialHash: "mock-cardano-stake-hash", + }), +})); + +jest.mock("@buildonspark/issuer-sdk", () => ({ + IssuerSparkWallet: { + initialize: jest.fn().mockResolvedValue({ + wallet: { + getIdentityPublicKey: jest.fn().mockResolvedValue("mock-spark-pub-key"), + getSparkAddress: jest.fn().mockResolvedValue("mock-spark-address"), + }, + }), + }, +})); + +// Mock encryption functions +const mockEncryptedData = JSON.stringify({ + ephemeralPublicKey: "mock-ephemeral-key", + iv: "mock-iv", + ciphertext: "mock-ciphertext", +}); + +jest.mock("../../functions", () => ({ + encryptWithPublicKey: jest.fn().mockResolvedValue(mockEncryptedData), + decryptWithPrivateKey: jest.fn().mockResolvedValue( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), +})); + +// Import after mocks +import { WalletDeveloperControlled } from "./index"; +import { Web3Sdk } from "../index"; +import { MeshWallet } from "@meshsdk/wallet"; +import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import { encryptWithPublicKey, decryptWithPrivateKey } from "../../functions"; +import { v4 as uuidv4 } from "uuid"; + +// Mock axios +jest.mock("axios", () => ({ + create: jest.fn().mockReturnValue({ + get: jest.fn(), + post: jest.fn(), + }), +})); + +// Helper to create mock SDK with configurable properties +function createMockSdk(overrides: { + network?: "mainnet" | "testnet"; + privateKey?: string | undefined; + axiosInstance?: { get: jest.Mock; post: jest.Mock }; +}) { + const axiosInstance = overrides.axiosInstance || { + get: jest.fn(), + post: jest.fn(), + }; + return { + projectId: "test-project-id", + apiKey: "test-api-key", + network: overrides.network ?? "testnet", + privateKey: overrides.privateKey ?? "mock-private-key", + axiosInstance, + providerFetcher: undefined, + providerSubmitter: undefined, + getProject: jest.fn().mockResolvedValue({ + id: "test-project-id", + publicKey: "mock-public-key", + }), + } as unknown as Web3Sdk; +} + +describe("WalletDeveloperControlled", () => { + let mockAxiosInstance: { + get: jest.Mock; + post: jest.Mock; + }; + let mockSdk: Web3Sdk; + + beforeEach(() => { + jest.clearAllMocks(); + + mockAxiosInstance = { + get: jest.fn(), + post: jest.fn(), + }; + + mockSdk = createMockSdk({ axiosInstance: mockAxiosInstance }); + }); + + describe("constructor", () => { + it("creates instance with sdk reference", () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + expect(wallet.sdk).toBe(mockSdk); + expect(wallet.cardano).toBeDefined(); + expect(wallet.sparkIssuer).toBeDefined(); + }); + }); + + describe("createWallet", () => { + beforeEach(() => { + mockAxiosInstance.post.mockResolvedValue({ status: 200, data: {} }); + }); + + it("creates a new multi-chain wallet", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.createWallet(); + + expect(result).toHaveProperty("info"); + expect(result).toHaveProperty("sparkIssuerWallet"); + expect(result).toHaveProperty("cardanoWallet"); + }); + + it("generates a new mnemonic using MeshWallet.brew", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet(); + + expect(MeshWallet.brew).toHaveBeenCalled(); + }); + + it("encrypts mnemonic with project public key", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet(); + + expect(encryptWithPublicKey).toHaveBeenCalledWith({ + publicKey: "mock-public-key", + data: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + }); + }); + + it("creates wallet with testnet configuration", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet(); + + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ networkId: 0 }), + ); + }); + + it("creates wallet with mainnet configuration", async () => { + const mainnetSdk = createMockSdk({ + network: "mainnet", + axiosInstance: mockAxiosInstance, + }); + const wallet = new WalletDeveloperControlled({ sdk: mainnetSdk }); + + await wallet.createWallet(); + + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ networkId: 1 }), + ); + }); + + it("initializes Spark wallets for both mainnet and regtest", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet(); + + expect(IssuerSparkWallet.initialize).toHaveBeenCalledTimes(2); + expect(IssuerSparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: { network: "MAINNET" }, + }), + ); + expect(IssuerSparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: { network: "REGTEST" }, + }), + ); + }); + + it("posts wallet data to API", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet(); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "api/project-wallet", + expect.objectContaining({ + id: "mock-wallet-id-uuid", + projectId: "test-project-id", + key: mockEncryptedData, + }), + ); + }); + + it("includes tags in wallet data when provided", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet({ tags: ["treasury", "main"] }); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "api/project-wallet", + expect.objectContaining({ + tags: ["treasury", "main"], + }), + ); + }); + + it("includes chain information in wallet data", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.createWallet(); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "api/project-wallet", + expect.objectContaining({ + chains: { + cardano: { + pubKeyHash: "mock-cardano-pub-key-hash", + stakeCredentialHash: "mock-cardano-stake-hash", + }, + spark: { + mainnetPublicKey: "mock-spark-pub-key", + regtestPublicKey: "mock-spark-pub-key", + }, + }, + }), + ); + }); + + it("returns regtest spark wallet for testnet", async () => { + const testnetSdk = createMockSdk({ + network: "testnet", + axiosInstance: mockAxiosInstance, + }); + const wallet = new WalletDeveloperControlled({ sdk: testnetSdk }); + + const result = await wallet.createWallet(); + + expect(result.sparkIssuerWallet).toBeDefined(); + }); + + it("throws if project public key not found", async () => { + (mockSdk.getProject as jest.Mock).mockResolvedValue({ + id: "test-project-id", + publicKey: null, + }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await expect(wallet.createWallet()).rejects.toThrow( + "Project public key not found", + ); + }); + + it("throws if API call fails", async () => { + mockAxiosInstance.post.mockResolvedValue({ status: 500 }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await expect(wallet.createWallet()).rejects.toThrow( + "Failed to create wallet", + ); + }); + }); + + describe("initWallet", () => { + const mockWalletInfo = { + id: "test-wallet-id", + projectId: "test-project-id", + tags: ["test"], + key: mockEncryptedData, + chains: { + cardano: { + pubKeyHash: "mock-cardano-pub-key-hash", + stakeCredentialHash: "mock-cardano-stake-hash", + }, + spark: { + mainnetPublicKey: "mock-spark-pub-key", + regtestPublicKey: "mock-spark-pub-key", + }, + }, + }; + + beforeEach(() => { + mockAxiosInstance.get.mockResolvedValue({ + status: 200, + data: mockWalletInfo, + }); + }); + + it("loads existing wallet by ID", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.initWallet("test-wallet-id"); + + expect(result).toHaveProperty("info"); + expect(result).toHaveProperty("sparkWallet"); + expect(result).toHaveProperty("cardanoWallet"); + }); + + it("calls API to get wallet info", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.initWallet("test-wallet-id"); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "api/project-wallet/test-project-id/test-wallet-id", + ); + }); + + it("decrypts mnemonic with private key", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.initWallet("test-wallet-id"); + + expect(decryptWithPrivateKey).toHaveBeenCalledWith({ + privateKey: "mock-private-key", + encryptedDataJSON: mockEncryptedData, + }); + }); + + it("creates MeshWallet with decrypted mnemonic", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.initWallet("test-wallet-id"); + + expect(MeshWallet).toHaveBeenCalledWith( + expect.objectContaining({ + key: { + type: "mnemonic", + words: [ + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "abandon", + "about", + ], + }, + }), + ); + }); + + it("initializes Spark wallet with correct network for testnet", async () => { + const testnetSdk = createMockSdk({ + network: "testnet", + axiosInstance: mockAxiosInstance, + }); + const wallet = new WalletDeveloperControlled({ sdk: testnetSdk }); + + await wallet.initWallet("test-wallet-id"); + + expect(IssuerSparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: { network: "REGTEST" }, + }), + ); + }); + + it("initializes Spark wallet with correct network for mainnet", async () => { + const mainnetSdk = createMockSdk({ + network: "mainnet", + axiosInstance: mockAxiosInstance, + }); + const wallet = new WalletDeveloperControlled({ sdk: mainnetSdk }); + + await wallet.initWallet("test-wallet-id"); + + expect(IssuerSparkWallet.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + options: { network: "MAINNET" }, + }), + ); + }); + + it("throws if private key not provided", async () => { + // Need to create SDK without privateKey AND ensure getProject is called + const noPrivateKeyAxios = { + get: jest.fn().mockResolvedValue({ + status: 200, + data: { + id: "test-wallet-id", + projectId: "test-project-id", + tags: [], + key: mockEncryptedData, + chains: { + cardano: { pubKeyHash: "mock", stakeCredentialHash: "mock" }, + spark: { mainnetPublicKey: "mock", regtestPublicKey: "mock" }, + }, + }, + }), + post: jest.fn(), + }; + const noPrivateKeySdk = { + projectId: "test-project-id", + apiKey: "test-api-key", + network: "testnet" as const, + privateKey: undefined, // No private key + axiosInstance: noPrivateKeyAxios, + providerFetcher: undefined, + providerSubmitter: undefined, + getProject: jest.fn().mockResolvedValue({ + id: "test-project-id", + publicKey: "mock-public-key", + }), + } as unknown as Web3Sdk; + const wallet = new WalletDeveloperControlled({ sdk: noPrivateKeySdk }); + + await expect(wallet.initWallet("test-wallet-id")).rejects.toThrow( + "Private key required to load developer-controlled wallet", + ); + }); + }); + + describe("getWallet", () => { + const mockWalletInfo = { + id: "test-wallet-id", + projectId: "test-project-id", + tags: [], + key: mockEncryptedData, + chains: { + cardano: { + pubKeyHash: "mock-cardano-pub-key-hash", + stakeCredentialHash: "mock-cardano-stake-hash", + }, + spark: { + mainnetPublicKey: "mock-spark-pub-key", + regtestPublicKey: "mock-spark-pub-key", + }, + }, + }; + + beforeEach(() => { + mockAxiosInstance.get.mockResolvedValue({ + status: 200, + data: mockWalletInfo, + }); + }); + + it("returns wallet instance for cardano chain", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.getWallet("test-wallet-id", "cardano"); + + expect(result).toHaveProperty("info"); + expect(result).toHaveProperty("cardanoWallet"); + }); + + it("returns wallet instance for spark chain", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.getWallet("test-wallet-id", "spark"); + + expect(result).toHaveProperty("info"); + expect(result).toHaveProperty("sparkIssuerWallet"); + }); + + it("decrypts mnemonic when private key available", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await wallet.getWallet("test-wallet-id", "cardano"); + + expect(decryptWithPrivateKey).toHaveBeenCalled(); + }); + + it("returns info only when no private key", async () => { + const noPrivateKeySdk = { + projectId: "test-project-id", + apiKey: "test-api-key", + network: "testnet" as const, + privateKey: undefined, // No private key + axiosInstance: mockAxiosInstance, + providerFetcher: undefined, + providerSubmitter: undefined, + getProject: jest.fn().mockResolvedValue({ + id: "test-project-id", + publicKey: "mock-public-key", + }), + } as unknown as Web3Sdk; + const wallet = new WalletDeveloperControlled({ sdk: noPrivateKeySdk }); + + const result = await wallet.getWallet("test-wallet-id", "cardano"); + + expect(result.info).toBeDefined(); + expect(result.cardanoWallet).toBeUndefined(); + }); + }); + + describe("getProjectWallet", () => { + it("fetches wallet by ID from API", async () => { + const mockWalletInfo = { + id: "test-wallet-id", + projectId: "test-project-id", + }; + mockAxiosInstance.get.mockResolvedValue({ + status: 200, + data: mockWalletInfo, + }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.getProjectWallet("test-wallet-id"); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "api/project-wallet/test-project-id/test-wallet-id", + ); + expect(result).toEqual(mockWalletInfo); + }); + + it("throws if wallet not found", async () => { + mockAxiosInstance.get.mockResolvedValue({ status: 404 }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await expect(wallet.getProjectWallet("nonexistent")).rejects.toThrow( + "Project wallet not found", + ); + }); + }); + + describe("getProjectWallets", () => { + it("fetches all wallets for project", async () => { + const mockWallets = [ + { id: "wallet-1", projectId: "test-project-id" }, + { id: "wallet-2", projectId: "test-project-id" }, + ]; + mockAxiosInstance.get.mockResolvedValue({ + status: 200, + data: mockWallets, + }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.getProjectWallets(); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "api/project-wallet/test-project-id", + ); + expect(result).toEqual(mockWallets); + }); + + it("returns empty array when no wallets", async () => { + mockAxiosInstance.get.mockResolvedValue({ status: 200, data: [] }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.getProjectWallets(); + + expect(result).toEqual([]); + }); + + it("throws if API call fails", async () => { + mockAxiosInstance.get.mockResolvedValue({ status: 500 }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await expect(wallet.getProjectWallets()).rejects.toThrow( + "Failed to get project wallets", + ); + }); + }); +}); + +describe("WalletDeveloperControlled error scenarios", () => { + let mockSdk: Web3Sdk; + let mockAxiosInstance: { get: jest.Mock; post: jest.Mock }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockAxiosInstance = { + get: jest.fn(), + post: jest.fn(), + }; + + mockSdk = { + projectId: "test-project-id", + apiKey: "test-api-key", + network: "testnet", + privateKey: "mock-private-key", + axiosInstance: mockAxiosInstance, + getProject: jest.fn().mockResolvedValue({ + id: "test-project-id", + publicKey: "mock-public-key", + }), + } as unknown as Web3Sdk; + }); + + it("handles network errors gracefully", async () => { + mockAxiosInstance.post.mockRejectedValue(new Error("Network error")); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await expect(wallet.createWallet()).rejects.toThrow("Network error"); + }); + + it("handles API timeout", async () => { + mockAxiosInstance.get.mockRejectedValue(new Error("timeout")); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + await expect(wallet.getProjectWallets()).rejects.toThrow("timeout"); + }); +}); diff --git a/src/types/core/multi-chain.ts b/src/types/core/multi-chain.ts index faf57f5..19a1cc1 100644 --- a/src/types/core/multi-chain.ts +++ b/src/types/core/multi-chain.ts @@ -1,5 +1,10 @@ import { MeshWallet } from "@meshsdk/wallet"; import { IssuerSparkWallet } from "@buildonspark/issuer-sdk"; +import { + MultiChainWalletInfo as ContractWalletInfo, + validateMultiChainWalletInfo, + isValidMultiChainWalletInfo, +} from "@utxos/api-contracts"; /** * Standardized network ID type (0 = testnet, 1 = mainnet) @@ -16,23 +21,12 @@ export interface MultiChainWalletOptions { /** * Multi-chain wallet information - one wallet per project with all chain keys + * Type is imported from shared API contracts to ensure SDK/API compatibility. */ -export interface MultiChainWalletInfo { - id: string; - projectId: string; - tags: string[]; - key: string; - chains: { - cardano?: { - pubKeyHash: string; - stakeCredentialHash: string; - }; - spark?: { - mainnetPublicKey: string; - regtestPublicKey: string; - }; - }; -} +export type MultiChainWalletInfo = ContractWalletInfo; + +// Re-export validation utilities for runtime checking +export { validateMultiChainWalletInfo, isValidMultiChainWalletInfo }; /** * Multi-chain wallet instance with initialized wallet objects From 5bf12aedba0088952cb5dfcc992589fa305d1099 Mon Sep 17 00:00:00 2001 From: Jingles Date: Fri, 23 Jan 2026 22:56:33 +0800 Subject: [PATCH 29/30] feat(wallet): add pagination support for getProjectWallets BREAKING CHANGES: - getProjectWallets() now returns { data, pagination } instead of array - Type renamed from Web3ProjectWallet to MultiChainWalletInfo - Wallet properties moved from flat to nested chains.cardano/spark Changes: - Add pagination support with optional page parameter - Add getAllProjectWallets() helper for fetching all pages - Update tests and examples for new response format - Bump version to 0.1.0 Co-Authored-By: Claude Opus 4.5 --- examples/developer-controlled-wallet/index.ts | 11 ++- package.json | 2 +- .../wallet-developer-controlled/index.test.ts | 69 +++++++++++++++-- src/sdk/wallet-developer-controlled/index.ts | 75 +++++++++++++++++-- 4 files changed, 142 insertions(+), 15 deletions(-) diff --git a/examples/developer-controlled-wallet/index.ts b/examples/developer-controlled-wallet/index.ts index 6c76146..0c53e4b 100644 --- a/examples/developer-controlled-wallet/index.ts +++ b/examples/developer-controlled-wallet/index.ts @@ -31,9 +31,14 @@ async function main() { // === LIST WALLETS === - console.log("\nListing all project wallets..."); - const wallets = await sdk.wallet.getProjectWallets(); - console.log(`Found ${wallets.length} wallets`); + console.log("\nListing project wallets (paginated)..."); + const { data: wallets, pagination } = await sdk.wallet.getProjectWallets(); + console.log(`Found ${wallets.length} wallets on page ${pagination.page} of ${pagination.totalPages}`); + console.log(`Total wallets: ${pagination.totalCount}`); + + // Or get all wallets at once + const allWallets = await sdk.wallet.getAllProjectWallets(); + console.log(`All wallets: ${allWallets.length}`); // === GET WALLET BY CHAIN === diff --git a/package.json b/package.json index 9e0ba7b..bbe1f08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@utxos/sdk", - "version": "0.0.78", + "version": "0.1.0", "description": "UTXOS SDK - Web3 infrastructure platform for UTXO blockchains", "main": "./dist/index.cjs", "browser": "./dist/index.js", diff --git a/src/sdk/wallet-developer-controlled/index.test.ts b/src/sdk/wallet-developer-controlled/index.test.ts index 51ec344..7be78f5 100644 --- a/src/sdk/wallet-developer-controlled/index.test.ts +++ b/src/sdk/wallet-developer-controlled/index.test.ts @@ -553,32 +553,61 @@ describe("WalletDeveloperControlled", () => { }); describe("getProjectWallets", () => { - it("fetches all wallets for project", async () => { + it("fetches wallets with pagination", async () => { const mockWallets = [ { id: "wallet-1", projectId: "test-project-id" }, { id: "wallet-2", projectId: "test-project-id" }, ]; + const mockResponse = { + data: mockWallets, + pagination: { page: 1, pageSize: 10, totalCount: 2, totalPages: 1 }, + }; mockAxiosInstance.get.mockResolvedValue({ status: 200, - data: mockWallets, + data: mockResponse, }); const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); const result = await wallet.getProjectWallets(); expect(mockAxiosInstance.get).toHaveBeenCalledWith( - "api/project-wallet/test-project-id", + "api/project-wallet/test-project-id?page=1", + ); + expect(result.data).toEqual(mockWallets); + expect(result.pagination.totalCount).toBe(2); + }); + + it("fetches specific page", async () => { + const mockResponse = { + data: [{ id: "wallet-3", projectId: "test-project-id" }], + pagination: { page: 2, pageSize: 10, totalCount: 4, totalPages: 2 }, + }; + mockAxiosInstance.get.mockResolvedValue({ + status: 200, + data: mockResponse, + }); + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + const result = await wallet.getProjectWallets({ page: 2 }); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "api/project-wallet/test-project-id?page=2", ); - expect(result).toEqual(mockWallets); + expect(result.pagination.page).toBe(2); }); it("returns empty array when no wallets", async () => { - mockAxiosInstance.get.mockResolvedValue({ status: 200, data: [] }); + const mockResponse = { + data: [], + pagination: { page: 1, pageSize: 10, totalCount: 0, totalPages: 0 }, + }; + mockAxiosInstance.get.mockResolvedValue({ status: 200, data: mockResponse }); const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); const result = await wallet.getProjectWallets(); - expect(result).toEqual([]); + expect(result.data).toEqual([]); + expect(result.pagination.totalCount).toBe(0); }); it("throws if API call fails", async () => { @@ -590,6 +619,34 @@ describe("WalletDeveloperControlled", () => { ); }); }); + + describe("getAllProjectWallets", () => { + it("fetches all wallets across pages", async () => { + const wallet = new WalletDeveloperControlled({ sdk: mockSdk }); + + // First page + mockAxiosInstance.get.mockResolvedValueOnce({ + status: 200, + data: { + data: [{ id: "wallet-1" }, { id: "wallet-2" }, { id: "wallet-3" }], + pagination: { page: 1, pageSize: 10, totalCount: 5, totalPages: 2 }, + }, + }); + // Second page + mockAxiosInstance.get.mockResolvedValueOnce({ + status: 200, + data: { + data: [{ id: "wallet-4" }, { id: "wallet-5" }], + pagination: { page: 2, pageSize: 10, totalCount: 5, totalPages: 2 }, + }, + }); + + const result = await wallet.getAllProjectWallets(); + + expect(result).toHaveLength(5); + expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2); + }); + }); }); describe("WalletDeveloperControlled error scenarios", () => { diff --git a/src/sdk/wallet-developer-controlled/index.ts b/src/sdk/wallet-developer-controlled/index.ts index c43aa9d..ffd5f87 100644 --- a/src/sdk/wallet-developer-controlled/index.ts +++ b/src/sdk/wallet-developer-controlled/index.ts @@ -289,21 +289,86 @@ export class WalletDeveloperControlled { } /** - * Retrieves all wallets for the project. + * Retrieves wallets for the project with pagination support. * - * @returns Promise that resolves to array of wallet info + * @param options - Pagination options + * @param options.page - Page number (default: 1) + * @param options.pageSize - Number of wallets per page (default: 10) + * @returns Promise that resolves to paginated wallet response + * + * @example + * ```typescript + * // Get first page of wallets + * const { data, pagination } = await sdk.wallet.getProjectWallets(); + * + * // Get specific page + * const { data, pagination } = await sdk.wallet.getProjectWallets({ page: 2 }); + * + * // Iterate through all pages + * let page = 1; + * let allWallets: MultiChainWalletInfo[] = []; + * while (true) { + * const { data, pagination } = await sdk.wallet.getProjectWallets({ page }); + * allWallets.push(...data); + * if (page >= pagination.totalPages) break; + * page++; + * } + * ``` */ - async getProjectWallets(): Promise { + async getProjectWallets(options?: { page?: number }): Promise<{ + data: MultiChainWalletInfo[]; + pagination: { + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + }; + }> { + const page = options?.page ?? 1; const { data, status } = await this.sdk.axiosInstance.get( - `api/project-wallet/${this.sdk.projectId}`, + `api/project-wallet/${this.sdk.projectId}?page=${page}`, ); if (status === 200) { - return data as MultiChainWalletInfo[]; + return data as { + data: MultiChainWalletInfo[]; + pagination: { + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + }; + }; } throw new Error("Failed to get project wallets"); } + + /** + * Retrieves all wallets for the project (fetches all pages). + * + * @returns Promise that resolves to array of all wallet info + * + * @example + * ```typescript + * const wallets = await sdk.wallet.getAllProjectWallets(); + * console.log(`Found ${wallets.length} wallets`); + * ``` + */ + async getAllProjectWallets(): Promise { + const allWallets: MultiChainWalletInfo[] = []; + let page = 1; + + while (true) { + const { data, pagination } = await this.getProjectWallets({ page }); + allWallets.push(...data); + + if (page >= pagination.totalPages) break; + page++; + } + + return allWallets; + } } export { CardanoWalletDeveloperControlled } from "./cardano"; From b65d668bda9671d565a2299ebc5523e5c7c0ca79 Mon Sep 17 00:00:00 2001 From: Jingles Date: Sat, 24 Jan 2026 01:08:48 +0800 Subject: [PATCH 30/30] fix(deps): pin api-contracts version to ^0.0.1 Using wildcard (*) caused npm resolution issues in CI due to registry propagation delays. Pinning to explicit version. Co-Authored-By: Claude Opus 4.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bbe1f08..affe03f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "@utxos/api-contracts": "*", + "@utxos/api-contracts": "^0.0.1", "@buildonspark/issuer-sdk": "^0.1.5", "@buildonspark/spark-sdk": "0.5.0", "@meshsdk/bitcoin": "1.9.0-beta.89",