From 0ef5514ebf73f2ec356b04e943411b6efc0bde57 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 25 Sep 2024 10:57:41 -0400 Subject: [PATCH 1/4] feat: add retry logic to keep selecting coins until it finds one --- src/blockchain/Wallet.ts | 133 ++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 35 deletions(-) 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; } From 3642458eb44611496f70cb26c12a411164b9e77d Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 25 Sep 2024 12:01:33 -0400 Subject: [PATCH 2/4] fix: public ip logic --- src/utils/network.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/utils/network.ts b/src/utils/network.ts index 0cfc0bd..61bfff7 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -3,12 +3,26 @@ 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; + if (isValidIp(publicIp)) { + return publicIp; + } else { + console.error('Invalid public IP address in environment variable'); + return undefined; + } } let attempt = 0; @@ -16,8 +30,15 @@ export const getPublicIpAddress = async (): Promise => { while (attempt < MAX_RETRIES) { try { 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; + const ipAddress = response.body.ip_address; + + if (isValidIp(ipAddress)) { + return ipAddress; + } else { + throw new Error('Invalid IP address format received'); + } } else { throw new Error('Failed to retrieve public IP address'); } From b722fa0c6391eb7cfebc49be5ce4e50fae44483f Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 25 Sep 2024 12:05:40 -0400 Subject: [PATCH 3/4] fix: public ip logic --- src/utils/network.ts | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/utils/network.ts b/src/utils/network.ts index 61bfff7..c0996a1 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -1,11 +1,16 @@ -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}|:)|::)$/; +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 => { @@ -16,38 +21,42 @@ export const getPublicIpAddress = async (): Promise => { const publicIp = process.env.PUBLIC_IP; if (publicIp) { - console.log('Public IP address from env:', publicIp); + console.log("Public IP address from env:", publicIp); if (isValidIp(publicIp)) { return publicIp; - } else { - console.error('Invalid public IP address in environment variable'); - return undefined; } + 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) { const ipAddress = response.body.ip_address; if (isValidIp(ipAddress)) { return ipAddress; - } else { - throw new Error('Invalid IP address format received'); } - } else { - throw new Error('Failed to retrieve public IP address'); + 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)); From 7c3a34da7aafbc37f7f1ac3daf2f8e288698450f Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 25 Sep 2024 12:06:03 -0400 Subject: [PATCH 4/4] chore(release): 0.0.1-alpha.86 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) 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",