diff --git a/CHANGELOG.md b/CHANGELOG.md index f62587b..ef5ca40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.0.1-alpha.86](https://github.com/DIG-Network/dig-chia-sdk/compare/v0.0.1-alpha.85...v0.0.1-alpha.86) (2024-09-25) + + +### Features + +* add retry logic to keep selecting coins until it finds one ([0ef5514](https://github.com/DIG-Network/dig-chia-sdk/commit/0ef5514ebf73f2ec356b04e943411b6efc0bde57)) + + +### Bug Fixes + +* public ip logic ([b722fa0](https://github.com/DIG-Network/dig-chia-sdk/commit/b722fa0c6391eb7cfebc49be5ce4e50fae44483f)) +* public ip logic ([3642458](https://github.com/DIG-Network/dig-chia-sdk/commit/3642458eb44611496f70cb26c12a411164b9e77d)) + ### [0.0.1-alpha.85](https://github.com/DIG-Network/dig-chia-sdk/compare/v0.0.1-alpha.84...v0.0.1-alpha.85) (2024-09-25) diff --git a/package-lock.json b/package-lock.json index 26c16b1..0c50801 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dignetwork/dig-sdk", - "version": "0.0.1-alpha.85", + "version": "0.0.1-alpha.86", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dignetwork/dig-sdk", - "version": "0.0.1-alpha.85", + "version": "0.0.1-alpha.86", "license": "ISC", "dependencies": { "@dignetwork/datalayer-driver": "^0.1.25", diff --git a/package.json b/package.json index abecac8..ab1962c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dignetwork/dig-sdk", - "version": "0.0.1-alpha.85", + "version": "0.0.1-alpha.86", "description": "", "type": "commonjs", "main": "./dist/index.js", diff --git a/src/blockchain/Wallet.ts b/src/blockchain/Wallet.ts index ff3027a..1c3357e 100644 --- a/src/blockchain/Wallet.ts +++ b/src/blockchain/Wallet.ts @@ -85,7 +85,10 @@ export class Wallet { return mnemonic; } - public static async importWallet(walletName: string, seed?: string): Promise { + public static async importWallet( + walletName: string, + seed?: string + ): Promise { const mnemonic = seed || (await askForMnemonicInput()).providedMnemonic; if (!bip39.validateMnemonic(mnemonic)) { throw new Error("Provided mnemonic is invalid."); @@ -94,7 +97,9 @@ export class Wallet { return mnemonic; } - public static async importWalletFromChia(walletName: string): Promise { + public static async importWalletFromChia( + walletName: string + ): Promise { const chiaRoot = getChiaRoot(); const certificateFolderPath = `${chiaRoot}/config/ssl`; const config = getChiaConfig(); @@ -169,10 +174,13 @@ export class Wallet { return Object.keys(config); } - private static async getWalletFromKeyring(walletName: string): Promise { + private static async getWalletFromKeyring( + walletName: string + ): Promise { const nconfManager = new NconfManager(KEYRING_FILE); if (await nconfManager.configExists()) { - const encryptedData: EncryptedData | null = await nconfManager.getConfigValue(walletName); + const encryptedData: EncryptedData | null = + await nconfManager.getConfigValue(walletName); if (encryptedData) { return decryptData(encryptedData); } @@ -180,7 +188,10 @@ export class Wallet { return null; } - private static async saveWalletToKeyring(walletName: string, mnemonic: string): Promise { + private static async saveWalletToKeyring( + walletName: string, + mnemonic: string + ): Promise { const nconfManager = new NconfManager(KEYRING_FILE); const encryptedData = encryptData(mnemonic); await nconfManager.setConfigValue(walletName, encryptedData); @@ -189,7 +200,10 @@ export class Wallet { public async createKeyOwnershipSignature(nonce: string): Promise { const message = `Signing this message to prove ownership of key.\n\nNonce: ${nonce}`; const privateSyntheticKey = await this.getPrivateSyntheticKey(); - const signature = signMessage(Buffer.from(message, "utf-8"), privateSyntheticKey); + const signature = signMessage( + Buffer.from(message, "utf-8"), + privateSyntheticKey + ); return signature.toString("hex"); } @@ -212,37 +226,76 @@ export class Wallet { feeBigInt: bigint, omitCoins: Coin[] = [] ): Promise { - const cache = new FileCache<{ coinId: string; expiry: number }>(path.join(USER_DIR_PATH, "reserved_coins")); - const cachedReservedCoins = cache.getCachedKeys(); - const now = Date.now(); - const omitCoinIds = omitCoins.map((coin) => getCoinId(coin).toString("hex")); - - cachedReservedCoins.forEach((coinId) => { - const reservation = cache.get(coinId); - if (reservation && reservation.expiry > now) { - omitCoinIds.push(coinId); - } else { - cache.delete(coinId); - } - }); - - const ownerPuzzleHash = await this.getOwnerPuzzleHash(); - - const coinsResp = await peer.getAllUnspentCoins( - ownerPuzzleHash, - MIN_HEIGHT, - Buffer.from(MIN_HEIGHT_HEADER_HASH, "hex") + const cache = new FileCache<{ coinId: string; expiry: number }>( + path.join(USER_DIR_PATH, "reserved_coins") ); - const unspentCoins = coinsResp.coins.filter( - (coin) => !omitCoinIds.includes(getCoinId(coin).toString("hex")) - ); + const ownerPuzzleHash = await this.getOwnerPuzzleHash(); - const selectedCoins = selectCoins(unspentCoins, feeBigInt + coinAmount); - if (selectedCoins.length === 0) { - throw new Error("No unspent coins available."); + // Define a function to attempt selecting unspent coins + const trySelectCoins = async (): Promise => { + const now = Date.now(); + const omitCoinIds = omitCoins.map((coin) => + getCoinId(coin).toString("hex") + ); + + // Update omitCoinIds with currently valid reserved coins + const cachedReservedCoins = cache.getCachedKeys(); + + cachedReservedCoins.forEach((coinId) => { + const reservation = cache.get(coinId); + if (reservation && reservation.expiry > now) { + if (!omitCoinIds.includes(coinId)) { + omitCoinIds.push(coinId); + } + } else { + cache.delete(coinId); + } + }); + + const coinsResp = await peer.getAllUnspentCoins( + ownerPuzzleHash, + MIN_HEIGHT, + Buffer.from(MIN_HEIGHT_HEADER_HASH, "hex") + ); + + const unspentCoins = coinsResp.coins.filter( + (coin) => !omitCoinIds.includes(getCoinId(coin).toString("hex")) + ); + + const selectedCoins = selectCoins(unspentCoins, feeBigInt + coinAmount); + return selectedCoins; + }; + + let selectedCoins: Coin[] = []; + let retry = true; + + while (retry) { + selectedCoins = await trySelectCoins(); + + if (selectedCoins.length > 0) { + // Coins have been successfully selected + retry = false; + } else { + const now = Date.now(); + // Check if there are any valid cached reserved coins left + const cachedReservedCoins = cache.getCachedKeys().filter((coinId) => { + const reservation = cache.get(coinId); + return reservation && reservation.expiry > now; + }); + + if (cachedReservedCoins.length > 0) { + // Wait 10 seconds and try again + console.log("No unspent coins available. Waiting 10 seconds..."); + await new Promise((resolve) => setTimeout(resolve, 10000)); + } else { + // No unspent coins and no reserved coins + throw new Error("No unspent coins available."); + } + } } + // Reserve the selected coins selectedCoins.forEach((coin) => { const coinId = getCoinId(coin).toString("hex"); cache.set(coinId, { coinId, expiry: Date.now() + CACHE_DURATION }); @@ -251,13 +304,23 @@ export class Wallet { return selectedCoins; } - public static async calculateFeeForCoinSpends(peer: Peer, coinSpends: CoinSpend[] | null): Promise { + public static async calculateFeeForCoinSpends( + peer: Peer, + coinSpends: CoinSpend[] | null + ): Promise { return BigInt(1000000); } - public static async isCoinSpendable(peer: Peer, coinId: Buffer): Promise { + public static async isCoinSpendable( + peer: Peer, + coinId: Buffer + ): Promise { try { - return await peer.isCoinSpent(coinId, MIN_HEIGHT, Buffer.from(MIN_HEIGHT_HEADER_HASH, "hex")); + return await peer.isCoinSpent( + coinId, + MIN_HEIGHT, + Buffer.from(MIN_HEIGHT_HEADER_HASH, "hex") + ); } catch (error) { return false; } diff --git a/src/utils/network.ts b/src/utils/network.ts index 0cfc0bd..c0996a1 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -1,32 +1,62 @@ -import superagent from 'superagent'; +/* + * Stopgap until better solution for finding public IPS found + */ +import superagent from "superagent"; const MAX_RETRIES = 5; const RETRY_DELAY = 2000; // in milliseconds +// Regular expression for validating both IPv4 and IPv6 addresses +const ipv4Regex = + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; +const ipv6Regex = + /^(([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:)|(([0-9a-fA-F]{1,4}:){1,7}|:):(([0-9a-fA-F]{1,4}:){1,6}|:):([0-9a-fA-F]{1,4}|:):([0-9a-fA-F]{1,4}|:)|::)$/; + +// Helper function to validate the IP address +const isValidIp = (ip: string): boolean => { + return ipv4Regex.test(ip) || ipv6Regex.test(ip); +}; + export const getPublicIpAddress = async (): Promise => { const publicIp = process.env.PUBLIC_IP; if (publicIp) { - console.log('Public IP address from env:', publicIp); - return publicIp; + console.log("Public IP address from env:", publicIp); + if (isValidIp(publicIp)) { + return publicIp; + } + console.error("Invalid public IP address in environment variable"); + return undefined; } let attempt = 0; while (attempt < MAX_RETRIES) { try { - const response = await superagent.get('https://api.datalayer.storage/user/v1/get_user_ip'); + const response = await superagent.get( + "https://api.datalayer.storage/user/v1/get_user_ip" + ); + if (response.body && response.body.success) { - return response.body.ip_address; - } else { - throw new Error('Failed to retrieve public IP address'); + const ipAddress = response.body.ip_address; + + if (isValidIp(ipAddress)) { + return ipAddress; + } + throw new Error("Invalid IP address format received"); } + throw new Error("Failed to retrieve public IP address"); } catch (error: any) { attempt++; - console.error(`Error fetching public IP address (Attempt ${attempt}):`, error.message); + console.error( + `Error fetching public IP address (Attempt ${attempt}):`, + error.message + ); if (attempt >= MAX_RETRIES) { - throw new Error('Could not retrieve public IP address after several attempts'); + throw new Error( + "Could not retrieve public IP address after several attempts" + ); } await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));