diff --git a/.gitignore b/.gitignore index d9c4166..e718acd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* src/config.ts -**/*.todo \ No newline at end of file +**/*.todo +src/test/**/* +src/data/**/* \ No newline at end of file diff --git a/package.json b/package.json index 1790d4d..431b7b7 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,12 @@ "description": "Raydium SDK V2 demo.", "license": "GPL-3.0", "dependencies": { - "@raydium-io/raydium-sdk-v2": "0.1.14-alpha", + "@raydium-io/raydium-sdk-v2": "0.1.15-alpha", "@solana/spl-token": "^0.4.6", + "@types/jsonfile": "^6.1.4", "bs58": "^5.0.0", "decimal.js": "^10.4.3", + "jsonfile": "^6.1.0", "node-cron": "^3.0.3", "ts-node": "^10.9.1", "typescript": "^5.3.3" diff --git a/src/amm/swap.ts b/src/amm/swap.ts index 1f78774..123f6da 100644 --- a/src/amm/swap.ts +++ b/src/amm/swap.ts @@ -29,6 +29,8 @@ export const swap = async () => { ...poolInfo, baseReserve: pool.baseReserve, quoteReserve: pool.quoteReserve, + status: pool.status.toNumber(), + version: 4, }, amountIn: new BN(amountIn), mintIn: poolInfo.mintA.address, // swap mintB -> mintA, use: poolInfo.mintB.address diff --git a/src/cache/utils.ts b/src/cache/utils.ts new file mode 100644 index 0000000..62d2287 --- /dev/null +++ b/src/cache/utils.ts @@ -0,0 +1,68 @@ +import { AmmPool, ClmmPool } from '@raydium-io/raydium-sdk-v2' +import { PublicKey } from '@solana/web3.js' +import jsonfile from 'jsonfile' + +const filePath = './src/data/pool_data.json' + +export const readCachePoolData = (cacheTime?: number) => { + let cacheData: { time: number; ammPools: AmmPool[]; clmmPools: ClmmPool[] } = { + time: 0, + ammPools: [], + clmmPools: [], + } + try { + console.log('reading cache pool data') + const data = jsonfile.readFileSync(filePath) as { time: number; ammPools: AmmPool[]; clmmPools: ClmmPool[] } + if (Date.now() - data.time > (cacheTime ?? 1000 * 60 * 10)) { + console.log('cache data expired') + return cacheData + } + cacheData.time = data.time + cacheData.ammPools = data.ammPools.map((p) => ({ + ...p, + id: new PublicKey(p.id), + mintA: new PublicKey(p.mintA), + mintB: new PublicKey(p.mintB), + })) + cacheData.clmmPools = data.clmmPools.map((p) => ({ + ...p, + id: new PublicKey(p.id), + mintA: new PublicKey(p.mintA), + mintB: new PublicKey(p.mintB), + })) + console.log('read cache pool data success') + } catch { + console.log('cannot read cache pool data') + } + + return { + ammPools: cacheData.ammPools, + clmmPools: cacheData.clmmPools, + } +} + +export const writeCachePoolData = (data: { ammPools: AmmPool[]; clmmPools: ClmmPool[] }) => { + console.log('caching all pool basic info..') + jsonfile + .writeFile(filePath, { + time: Date.now(), + ammPools: data.ammPools.map((p) => ({ + id: p.id.toBase58(), + version: p.version, + mintA: p.mintA.toBase58(), + mintB: p.mintB.toBase58(), + })), + clmmPools: data.clmmPools.map((p) => ({ + id: p.id.toBase58(), + version: p.version, + mintA: p.mintA.toBase58(), + mintB: p.mintB.toBase58(), + })), + }) + .then(() => { + console.log('cache pool data success') + }) + .catch((e) => { + console.log('cache pool data failed', e) + }) +} diff --git a/src/trade/routeSwap.ts b/src/trade/routeSwap.ts new file mode 100644 index 0000000..e114849 --- /dev/null +++ b/src/trade/routeSwap.ts @@ -0,0 +1,117 @@ +import { + WSOLMint, + RAYMint, + USDCMint, + toFeeConfig, + toApiV3Token, + Router, + TokenAmount, + Token, +} from '@raydium-io/raydium-sdk-v2' +import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token' +import { initSdk, txVersion } from '../config' +import { readCachePoolData, writeCachePoolData } from '../cache/utils' + +async function routeSwap() { + const raydium = await initSdk() + await raydium.fetchChainTime() + + const inputAmount = '100' + const SOL = NATIVE_MINT // or WSOLMint + const [inputMint, outputMint] = [SOL, USDCMint] + const [inputMintStr, outputMintStr] = [inputMint.toBase58(), outputMint.toBase58()] + + // strongly recommend cache all pool data, it will reduce lots of data fetching time + // code below is a simple way to cache it, you can implement it with any other ways + let poolData = readCachePoolData() // initial cache time is 10 mins(1000 * 60 * 10), if wants to cache longer, set bigger number in milliseconds + if (poolData.ammPools.length === 0) { + console.log('fetching all pool basic info, this might take a while (more than 30 seconds)..') + poolData = await raydium.tradeV2.fetchRoutePoolBasicInfo() + writeCachePoolData(poolData) + } + + console.log('computing swap route..') + const routes = raydium.tradeV2.getAllRoute({ + inputMint, + outputMint, + ...poolData, + }) + + const { + routePathDict, + mintInfos, + ammPoolsRpcInfo, + ammSimulateCache, + clmmPoolsRpcInfo, + computeClmmPoolInfo, + computePoolTickData, + } = await raydium.tradeV2.fetchSwapRoutesData({ + routes, + inputMint, + outputMint, + }) + + console.log('calculating available swap routes...') + const r = raydium.tradeV2.getAllRouteComputeAmountOut({ + inputTokenAmount: new TokenAmount( + new Token({ + mint: inputMintStr, + decimals: mintInfos[inputMintStr].decimals, + isToken2022: mintInfos[inputMintStr].programId.equals(TOKEN_2022_PROGRAM_ID), + }), + inputAmount + ), + directPath: routes.directPath.map((p) => ammSimulateCache[p.id.toBase58()] || computeClmmPoolInfo[p.id.toBase58()]), + routePathDict, + simulateCache: ammSimulateCache, + tickCache: computePoolTickData, + mintInfos: mintInfos, + outputToken: toApiV3Token({ + ...mintInfos[outputMintStr], + programId: mintInfos[outputMintStr].programId.toBase58(), + address: outputMintStr, + extensions: { + feeConfig: toFeeConfig(mintInfos[outputMintStr].feeConfig), + }, + }), + chainTime: Math.floor(raydium.chainTimeData?.chainTime ?? Date.now() / 1000), + slippage: 0.005, + epochInfo: await raydium.connection.getEpochInfo(), + }) + + console.log('best swap route:', { + input: r[0].amountIn.amount.toExact(), + output: r[0].amountOut.amount.toExact(), + swapType: r[0].routeType, + route: r[0].poolInfoList.map((p) => p.id).join(' -> '), + }) + + console.log('fetching swap route pool keys..') + const poolKeys = await raydium.tradeV2.computePoolToPoolKeys({ + pools: r[0].poolInfoList, + ammRpcData: ammPoolsRpcInfo, + clmmRpcData: clmmPoolsRpcInfo, + }) + + console.log('build swap tx..') + const { execute } = await raydium.tradeV2.swap({ + routeProgram: Router, + txVersion, + swapInfo: r[0], + swapPoolKeys: poolKeys, + ownerInfo: { + associatedOnly: true, + checkCreateATAOwner: true, + }, + computeBudgetConfig: { + units: 600000, + microLamports: 100000, + }, + }) + + console.log('execute tx..') + const { txIds } = await execute({ sequentially: true }) + console.log('txIds:', txIds) +} +/** uncomment code below to execute */ +routeSwap() diff --git a/yarn.lock b/yarn.lock index 8e7075e..987a80d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@babel/runtime@^7.10.5", "@babel/runtime@^7.24.5": +"@babel/runtime@^7.10.5", "@babel/runtime@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== @@ -222,10 +229,10 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" -"@raydium-io/raydium-sdk-v2@0.1.14-alpha": - version "0.1.14-alpha" - resolved "https://registry.yarnpkg.com/@raydium-io/raydium-sdk-v2/-/raydium-sdk-v2-0.1.14-alpha.tgz#f31eaa9d6e99324c4e17b3f4577432862e3bbe3b" - integrity sha512-b+UiJ7kFDoFxk1I6+Su10CUjrBsRc19A6vx3fhMbQvuBIT36SrRID3qqLg/2vYG36A4r2WGcQVW6Fuc9gBvMWA== +"@raydium-io/raydium-sdk-v2@0.1.15-alpha": + version "0.1.15-alpha" + resolved "https://registry.yarnpkg.com/@raydium-io/raydium-sdk-v2/-/raydium-sdk-v2-0.1.15-alpha.tgz#6c41948e07443939412f1a378906e1e4cd72d158" + integrity sha512-2zfOVrB8lSupQsfunKYFU/FAZHORPDZUzLXYAn0cDqxTgA3dGz+KxYiQptPB1ZN3yGVf+50J4GDBUM0HqBFYKg== dependencies: "@project-serum/serum" "^0.13.65" "@solana/buffer-layout" "^4.0.1" @@ -388,7 +395,28 @@ dependencies: buffer "^6.0.3" -"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.91.8": +"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.91.8": + version "1.93.0" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.93.0.tgz#4b6975020993cec2f6626e4f2bf559ca042df8db" + integrity sha512-suf4VYwWxERz4tKoPpXCRHFRNst7jmcFUaD65kII+zg9urpy5PeeqgLV6G5eWGzcVzA9tZeXOju1A1Y+0ojEVw== + dependencies: + "@babel/runtime" "^7.24.7" + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.0" + node-fetch "^2.7.0" + rpc-websockets "^9.0.0" + superstruct "^1.0.4" + +"@solana/web3.js@^1.32.0": version "1.91.8" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.91.8.tgz#0d5eb69626a92c391b53e15bfbb0bad3f6858e51" integrity sha512-USa6OS1jbh8zOapRJ/CBZImZ8Xb7AJjROZl5adql9TpOoBN9BUzyyouS5oPuZHft7S7eB8uJPuXWYjMi6BHgOw== @@ -409,6 +437,13 @@ rpc-websockets "^7.11.0" superstruct "^0.14.2" +"@swc/helpers@^0.5.11": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7" + integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A== + dependencies: + tslib "^2.4.0" + "@szmarczak/http-timer@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" @@ -473,6 +508,13 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== +"@types/jsonfile@^6.1.4": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + dependencies: + "@types/node" "*" + "@types/node@*": version "18.11.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" @@ -483,6 +525,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/ws@^7.4.4": version "7.4.7" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" @@ -490,6 +537,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.2.2": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -607,9 +661,9 @@ asynckit@^0.4.0: integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== axios@^1.1.3: - version "1.6.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" - integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -1106,6 +1160,11 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + eyes@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" @@ -1338,7 +1397,7 @@ graceful-fs@4.2.10: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.2.6: +graceful-fs@^4.1.6, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1643,6 +1702,15 @@ json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonlines@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/jsonlines/-/jsonlines-0.1.1.tgz#4fcd246dc5d0e38691907c44ab002f782d1d94cc" @@ -2449,6 +2517,22 @@ rpc-websockets@^7.11.0: bufferutil "^4.0.1" utf-8-validate "^5.0.2" +rpc-websockets@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.0.1.tgz#a69c83ffd5e26bdd6117f5b434ae68133c4805b6" + integrity sha512-JCkdc/TfJBGRfmjIFK7cmqX79nwPWUd9xCM0DAydRbdLShsW3j/GV2gmPlaFa8V1+2u4V/O47fm4ZR5+F6HyDw== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -2706,6 +2790,11 @@ superstruct@^0.14.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== +superstruct@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.4.tgz#0adb99a7578bd2f1c526220da6571b2d485d91ca" + integrity sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ== + tar@^6.1.11, tar@^6.1.2: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" @@ -2774,10 +2863,10 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^2.0.3: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.0.3, tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tuf-js@^1.1.7: version "1.1.7" @@ -2845,6 +2934,11 @@ unique-string@^3.0.0: dependencies: crypto-random-string "^4.0.0" +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"